1#!/usr/bin/python
2
3# pylint: disable=missing-docstring
4
5import logging
6import os
7import shutil
8import stat
9import tempfile
10import unittest
11
12import common
13from autotest_lib.client.common_lib import base_job, error
14
15
16class stub_job_directory(object):
17    """
18    Stub job_directory class, for replacing the job._job_directory factory.
19    Just creates a job_directory object without any of the actual directory
20    checks. When given None it creates a temporary name (but not an actual
21    temporary directory).
22    """
23    def __init__(self, path, is_writable=False):
24        # path=None and is_writable=False is always an error
25        assert path or is_writable
26
27        if path is None and is_writable:
28            self.path = tempfile.mktemp()
29        else:
30            self.path = path
31
32
33class stub_job_state(base_job.job_state):
34    """
35    Stub job state class, for replacing the job._job_state factory.
36    Doesn't actually provide any persistence, just the state handling.
37    """
38    def __init__(self):
39        self._state = {}
40        self._backing_file_lock = None
41
42    def read_from_file(self, file_path):
43        pass
44
45    def write_to_file(self, file_path):
46        pass
47
48    def set_backing_file(self, file_path):
49        pass
50
51    def _read_from_backing_file(self):
52        pass
53
54    def _write_to_backing_file(self):
55        pass
56
57    def _lock_backing_file(self):
58        pass
59
60    def _unlock_backing_file(self):
61        pass
62
63
64class test_init(unittest.TestCase):
65    class generic_tests(object):
66        """
67        Generic tests for any implementation of __init__.
68
69        Expectations:
70            A self.job attribute where self.job is a __new__'ed instance of
71            the job class to be tested, but not yet __init__'ed.
72
73            A self.call_init method that will config the appropriate mocks
74            and then call job.__init__. It should undo any mocks it created
75            afterwards.
76        """
77
78        PUBLIC_ATTRIBUTES = set([
79            # standard directories
80            'autodir', 'clientdir', 'serverdir', 'resultdir', 'pkgdir',
81            'tmpdir', 'testdir', 'site_testdir', 'bindir',
82            'profdir', 'toolsdir',
83
84            # other special attributes
85            'args', 'automatic_test_tag', 'control',
86            'default_profile_only', 'drop_caches',
87            'drop_caches_between_iterations', 'harness', 'hosts',
88            'logging', 'machines', 'num_tests_failed', 'num_tests_run',
89            'pkgmgr', 'profilers', 'resultdir', 'run_test_cleanup',
90            'sysinfo', 'tag', 'user', 'use_sequence_number',
91            'warning_loggers', 'warning_manager', 'label', 'test_retry',
92            'parent_job_id', 'in_lab', 'machine_dict_list'
93            ])
94
95        OPTIONAL_ATTRIBUTES = set([
96            'serverdir',
97
98            'automatic_test_tag', 'control', 'harness', 'num_tests_run',
99            'num_tests_failed', 'tag', 'warning_manager',
100            'warning_loggers', 'label', 'test_retry', 'parent_job_id'
101            ])
102
103        OPTIONAL_ATTRIBUTES_DEVICE_ERROR = set(['failed_with_device_error'])
104
105        def test_public_attributes_initialized(self):
106            # only the known public attributes should be there after __init__
107            self.call_init()
108            public_attributes = set(attr for attr in dir(self.job)
109                                    if not attr.startswith('_')
110                                    and not callable(getattr(self.job, attr)))
111            expected_attributes = self.PUBLIC_ATTRIBUTES
112            missing_attributes = expected_attributes - public_attributes
113            self.assertEqual(missing_attributes, set([]),
114                             'Missing attributes: %s' %
115                             ', '.join(sorted(missing_attributes)))
116            extra_attributes = (public_attributes - expected_attributes -
117                                self.OPTIONAL_ATTRIBUTES_DEVICE_ERROR)
118            self.assertEqual(extra_attributes, set([]),
119                             'Extra public attributes found: %s' %
120                             ', '.join(sorted(extra_attributes)))
121
122
123        def test_required_attributes_not_none(self):
124            required_attributes = (self.PUBLIC_ATTRIBUTES -
125                                   self.OPTIONAL_ATTRIBUTES)
126            self.call_init()
127            for attribute in required_attributes:
128                self.assertNotEqual(getattr(self.job, attribute, None), None,
129                                    'job.%s is None but is not optional'
130                                    % attribute)
131
132
133class test_find_base_directories(unittest.TestCase):
134    class generic_tests(object):
135        """
136        Generic tests for any implementation of _find_base_directories.
137
138        Expectations:
139            A self.job attribute where self.job is an instance of the job
140            class to be tested.
141        """
142        def test_autodir_is_not_none(self):
143            auto, client, server = self.job._find_base_directories()
144            self.assertNotEqual(auto, None)
145
146
147        def test_clientdir_is_not_none(self):
148            auto, client, server = self.job._find_base_directories()
149            self.assertNotEqual(client, None)
150
151
152class test_initialize_dir_properties(unittest.TestCase):
153    def make_job(self, autodir, server):
154        job = base_job.base_job.__new__(base_job.base_job)
155        job._job_directory = stub_job_directory
156        job._autodir = stub_job_directory(autodir)
157        if server:
158            job._clientdir = stub_job_directory(
159                os.path.join(autodir, 'client'))
160            job._serverdir = stub_job_directory(
161                os.path.join(autodir, 'server'))
162        else:
163            job._clientdir = stub_job_directory(job.autodir)
164            job._serverdir = None
165        return job
166
167
168    def setUp(self):
169        self.cjob = self.make_job('/atest/client', False)
170        self.sjob = self.make_job('/atest', True)
171
172
173    def test_always_client_dirs(self):
174        self.cjob._initialize_dir_properties()
175        self.sjob._initialize_dir_properties()
176
177        # check all the always-client dir properties
178        self.assertEqual(self.cjob.bindir, self.sjob.bindir)
179        self.assertEqual(self.cjob.profdir, self.sjob.profdir)
180        self.assertEqual(self.cjob.pkgdir, self.sjob.pkgdir)
181
182
183    def test_dynamic_dirs(self):
184        self.cjob._initialize_dir_properties()
185        self.sjob._initialize_dir_properties()
186
187        # check all the context-specifc dir properties
188        self.assert_(self.cjob.tmpdir.startswith('/atest/client'))
189        self.assert_(self.cjob.testdir.startswith('/atest/client'))
190        self.assert_(self.cjob.site_testdir.startswith('/atest/client'))
191        self.assertEqual(self.sjob.tmpdir, tempfile.gettempdir())
192        self.assert_(self.sjob.testdir.startswith('/atest/server'))
193        self.assert_(self.sjob.site_testdir.startswith('/atest/server'))
194
195
196class test_execution_context(unittest.TestCase):
197    def setUp(self):
198        clientdir = os.path.abspath(os.path.join(__file__, '..', '..'))
199        self.resultdir = tempfile.mkdtemp(suffix='unittest')
200        self.job = base_job.base_job.__new__(base_job.base_job)
201        self.job._find_base_directories = lambda: (clientdir, clientdir, None)
202        self.job._find_resultdir = lambda *_: self.resultdir
203        self.job.__init__()
204
205
206    def tearDown(self):
207        shutil.rmtree(self.resultdir, ignore_errors=True)
208
209
210    def test_pop_fails_without_push(self):
211        self.assertRaises(IndexError, self.job.pop_execution_context)
212
213
214    def test_push_changes_to_subdir(self):
215        sub1 = os.path.join(self.resultdir, 'sub1')
216        os.mkdir(sub1)
217        self.job.push_execution_context('sub1')
218        self.assertEqual(self.job.resultdir, sub1)
219
220
221    def test_push_creates_subdir(self):
222        sub2 = os.path.join(self.resultdir, 'sub2')
223        self.job.push_execution_context('sub2')
224        self.assertEqual(self.job.resultdir, sub2)
225        self.assert_(os.path.exists(sub2))
226
227
228    def test_push_handles_absolute_paths(self):
229        otherresults = tempfile.mkdtemp(suffix='unittest')
230        try:
231            self.job.push_execution_context(otherresults)
232            self.assertEqual(self.job.resultdir, otherresults)
233        finally:
234            shutil.rmtree(otherresults, ignore_errors=True)
235
236
237    def test_pop_restores_context(self):
238        sub3 = os.path.join(self.resultdir, 'sub3')
239        self.job.push_execution_context('sub3')
240        self.assertEqual(self.job.resultdir, sub3)
241        self.job.pop_execution_context()
242        self.assertEqual(self.job.resultdir, self.resultdir)
243
244
245    def test_push_and_pop_are_fifo(self):
246        sub4 = os.path.join(self.resultdir, 'sub4')
247        subsub = os.path.join(sub4, 'subsub')
248        self.job.push_execution_context('sub4')
249        self.assertEqual(self.job.resultdir, sub4)
250        self.job.push_execution_context('subsub')
251        self.assertEqual(self.job.resultdir, subsub)
252        self.job.pop_execution_context()
253        self.assertEqual(self.job.resultdir, sub4)
254        self.job.pop_execution_context()
255        self.assertEqual(self.job.resultdir, self.resultdir)
256
257
258class test_job_directory(unittest.TestCase):
259    def setUp(self):
260        self.testdir = tempfile.mkdtemp(suffix='unittest')
261        self.original_wd = os.getcwd()
262        os.chdir(self.testdir)
263
264
265    def tearDown(self):
266        os.chdir(self.original_wd)
267        shutil.rmtree(self.testdir, ignore_errors=True)
268
269
270    def test_passes_if_dir_exists(self):
271        os.mkdir('testing')
272        self.assert_(os.path.isdir('testing'))
273        jd = base_job.job_directory('testing')
274        self.assert_(os.path.isdir('testing'))
275
276
277    def test_fails_if_not_writable_and_dir_doesnt_exist(self):
278        self.assert_(not os.path.isdir('testing2'))
279        self.assertRaises(base_job.job_directory.MissingDirectoryException,
280                          base_job.job_directory, 'testing2')
281
282
283    def test_fails_if_file_already_exists(self):
284        open('testing3', 'w').close()
285        self.assert_(os.path.isfile('testing3'))
286        self.assertRaises(base_job.job_directory.MissingDirectoryException,
287                          base_job.job_directory, 'testing3')
288
289
290    def test_passes_if_writable_and_dir_exists(self):
291        os.mkdir('testing4')
292        self.assert_(os.path.isdir('testing4'))
293        jd = base_job.job_directory('testing4', True)
294        self.assert_(os.path.isdir('testing4'))
295
296
297    def test_creates_dir_if_writable_and_dir_doesnt_exist(self):
298        self.assert_(not os.path.isdir('testing5'))
299        jd = base_job.job_directory('testing5', True)
300        self.assert_(os.path.isdir('testing5'))
301
302
303    def test_recursive_creates_dir_if_writable_and_dir_doesnt_exist(self):
304        self.assert_(not os.path.isdir('testing6'))
305        base_job.job_directory('testing6/subdir', True)
306        self.assert_(os.path.isdir('testing6/subdir'))
307
308
309    def test_fails_if_writable_and_file_exists(self):
310        open('testing7', 'w').close()
311        self.assert_(os.path.isfile('testing7'))
312        self.assert_(not os.path.isdir('testing7'))
313        self.assertRaises(base_job.job_directory.UncreatableDirectoryException,
314                          base_job.job_directory, 'testing7', True)
315
316
317    def test_fails_if_writable_and_no_permission_to_create(self):
318        os.mkdir('testing8', 0555)
319        self.assert_(os.path.isdir('testing8'))
320        self.assertRaises(base_job.job_directory.UncreatableDirectoryException,
321                          base_job.job_directory, 'testing8/subdir', True)
322
323
324    def test_passes_if_not_is_writable_and_dir_not_writable(self):
325        os.mkdir('testing9', 0555)
326        self.assert_(os.path.isdir('testing9'))
327        self.assert_(not os.access('testing9', os.W_OK))
328        jd = base_job.job_directory('testing9')
329
330
331    def test_fails_if_is_writable_but_dir_not_writable(self):
332        os.mkdir('testing10', 0555)
333        self.assert_(os.path.isdir('testing10'))
334        self.assert_(not os.access('testing10', os.W_OK))
335        self.assertRaises(base_job.job_directory.UnwritableDirectoryException,
336                          base_job.job_directory, 'testing10', True)
337
338
339    def test_fails_if_no_path_and_not_writable(self):
340        self.assertRaises(base_job.job_directory.MissingDirectoryException,
341                          base_job.job_directory, None)
342
343
344    def test_no_path_and_and_not_writable_creates_tempdir(self):
345        jd = base_job.job_directory(None, True)
346        self.assert_(os.path.isdir(jd.path))
347        self.assert_(os.access(jd.path, os.W_OK))
348        temp_path = jd.path
349        del jd
350        self.assert_(not os.path.isdir(temp_path))
351
352
353class test_job_state(unittest.TestCase):
354    def setUp(self):
355        self.state = base_job.job_state()
356
357
358    def test_undefined_name_returns_key_error(self):
359        self.assertRaises(KeyError, self.state.get, 'ns1', 'missing_name')
360
361
362    def test_undefined_name_returns_default(self):
363        self.assertEqual(42, self.state.get('ns2', 'missing_name', default=42))
364
365
366    def test_none_is_valid_default(self):
367        self.assertEqual(None, self.state.get('ns3', 'missing_name',
368                                              default=None))
369
370
371    def test_get_returns_set_values(self):
372        self.state.set('ns4', 'name1', 50)
373        self.assertEqual(50, self.state.get('ns4', 'name1'))
374
375
376    def test_get_ignores_default_when_value_is_defined(self):
377        self.state.set('ns5', 'name2', 55)
378        self.assertEqual(55, self.state.get('ns5', 'name2', default=45))
379
380
381    def test_set_only_sets_one_value(self):
382        self.state.set('ns6', 'name3', 50)
383        self.assertEqual(50, self.state.get('ns6', 'name3'))
384        self.assertRaises(KeyError, self.state.get, 'ns6', 'name4')
385
386
387    def test_set_works_with_multiple_names(self):
388        self.state.set('ns7', 'name5', 60)
389        self.state.set('ns7', 'name6', 70)
390        self.assertEquals(60, self.state.get('ns7', 'name5'))
391        self.assertEquals(70, self.state.get('ns7', 'name6'))
392
393
394    def test_multiple_sets_override_each_other(self):
395        self.state.set('ns8', 'name7', 10)
396        self.state.set('ns8', 'name7', 25)
397        self.assertEquals(25, self.state.get('ns8', 'name7'))
398
399
400    def test_get_with_default_does_not_set(self):
401        self.assertEquals(100, self.state.get('ns9', 'name8', default=100))
402        self.assertRaises(KeyError, self.state.get, 'ns9', 'name8')
403
404
405    def test_set_in_one_namespace_ignores_other(self):
406        self.state.set('ns10', 'name9', 200)
407        self.assertEquals(200, self.state.get('ns10', 'name9'))
408        self.assertRaises(KeyError, self.state.get, 'ns11', 'name9')
409
410
411    def test_namespaces_do_not_collide(self):
412        self.state.set('ns12', 'name10', 250)
413        self.state.set('ns13', 'name10', -150)
414        self.assertEquals(-150, self.state.get('ns13', 'name10'))
415        self.assertEquals(250, self.state.get('ns12', 'name10'))
416
417
418    def test_discard_does_nothing_on_undefined_namespace(self):
419        self.state.discard('missing_ns', 'missing')
420        self.assertRaises(KeyError, self.state.get, 'missing_ns', 'missing')
421
422
423    def test_discard_does_nothing_on_missing_name(self):
424        self.state.set('ns14', 'name20', 111)
425        self.state.discard('ns14', 'missing')
426        self.assertEqual(111, self.state.get('ns14', 'name20'))
427        self.assertRaises(KeyError, self.state.get, 'ns14', 'missing')
428
429
430    def test_discard_deletes_name(self):
431        self.state.set('ns15', 'name21', 4567)
432        self.assertEqual(4567, self.state.get('ns15', 'name21'))
433        self.state.discard('ns15', 'name21')
434        self.assertRaises(KeyError, self.state.get, 'ns15', 'name21')
435
436
437    def test_discard_doesnt_touch_other_values(self):
438        self.state.set('ns16_1', 'name22', 'val1')
439        self.state.set('ns16_1', 'name23', 'val2')
440        self.state.set('ns16_2', 'name23', 'val3')
441        self.assertEqual('val1', self.state.get('ns16_1', 'name22'))
442        self.assertEqual('val3', self.state.get('ns16_2', 'name23'))
443        self.state.discard('ns16_1', 'name23')
444        self.assertEqual('val1', self.state.get('ns16_1', 'name22'))
445        self.assertEqual('val3', self.state.get('ns16_2', 'name23'))
446
447
448    def test_has_is_true_for_all_set_values(self):
449        self.state.set('ns17_1', 'name24', 1)
450        self.state.set('ns17_1', 'name25', 2)
451        self.state.set('ns17_2', 'name25', 3)
452        self.assert_(self.state.has('ns17_1', 'name24'))
453        self.assert_(self.state.has('ns17_1', 'name25'))
454        self.assert_(self.state.has('ns17_2', 'name25'))
455
456
457    def test_has_is_false_for_all_unset_values(self):
458        self.state.set('ns18_1', 'name26', 1)
459        self.state.set('ns18_1', 'name27', 2)
460        self.state.set('ns18_2', 'name27', 3)
461        self.assert_(not self.state.has('ns18_2', 'name26'))
462
463
464    def test_discard_namespace_drops_all_values(self):
465        self.state.set('ns19', 'var1', 10)
466        self.state.set('ns19', 'var3', 100)
467        self.state.discard_namespace('ns19')
468        self.assertRaises(KeyError, self.state.get, 'ns19', 'var1')
469        self.assertRaises(KeyError, self.state.get, 'ns19', 'var3')
470
471
472    def test_discard_namespace_works_on_missing_namespace(self):
473        self.state.discard_namespace('missing_ns')
474
475
476    def test_discard_namespace_doesnt_touch_other_values(self):
477        self.state.set('ns20', 'var1', 20)
478        self.state.set('ns20', 'var2', 200)
479        self.state.set('ns21', 'var2', 21)
480        self.state.discard_namespace('ns20')
481        self.assertEqual(21, self.state.get('ns21', 'var2'))
482
483
484# run the same tests as test_job_state, but with a backing file turned on
485# also adds some tests to check that each method is persistent
486class test_job_state_with_backing_file(test_job_state):
487    def setUp(self):
488        self.backing_file = tempfile.mktemp()
489        self.state = base_job.job_state()
490        self.state.set_backing_file(self.backing_file)
491
492
493    def tearDown(self):
494        if os.path.exists(self.backing_file):
495            os.remove(self.backing_file)
496
497
498    def test_set_is_persistent(self):
499        self.state.set('persist', 'var', 'value')
500        written_state = base_job.job_state()
501        written_state.read_from_file(self.backing_file)
502        self.assertEqual('value', written_state.get('persist', 'var'))
503
504
505    def test_discard_is_persistent(self):
506        self.state.set('persist', 'var', 'value')
507        self.state.discard('persist', 'var')
508        written_state = base_job.job_state()
509        written_state.read_from_file(self.backing_file)
510        self.assertRaises(KeyError, written_state.get, 'persist', 'var')
511
512
513    def test_discard_namespace_is_persistent(self):
514        self.state.set('persist', 'var', 'value')
515        self.state.discard_namespace('persist')
516        written_state = base_job.job_state()
517        written_state.read_from_file(self.backing_file)
518        self.assertRaises(KeyError, written_state.get, 'persist', 'var')
519
520
521class test_job_state_read_write_file(unittest.TestCase):
522    def setUp(self):
523        self.testdir = tempfile.mkdtemp(suffix='unittest')
524        self.original_wd = os.getcwd()
525        os.chdir(self.testdir)
526
527
528    def tearDown(self):
529        os.chdir(self.original_wd)
530        shutil.rmtree(self.testdir, ignore_errors=True)
531
532
533    def test_write_read_transfers_all_state(self):
534        state1 = base_job.job_state()
535        state1.set('ns1', 'var0', 50)
536        state1.set('ns2', 'var10', 100)
537        state1.write_to_file('transfer_file')
538        state2 = base_job.job_state()
539        self.assertRaises(KeyError, state2.get, 'ns1', 'var0')
540        self.assertRaises(KeyError, state2.get, 'ns2', 'var10')
541        state2.read_from_file('transfer_file')
542        self.assertEqual(50, state2.get('ns1', 'var0'))
543        self.assertEqual(100, state2.get('ns2', 'var10'))
544
545
546    def test_read_overwrites_in_memory(self):
547        state = base_job.job_state()
548        state.set('ns', 'myvar', 'hello')
549        state.write_to_file('backup')
550        state.set('ns', 'myvar', 'goodbye')
551        self.assertEqual('goodbye', state.get('ns', 'myvar'))
552        state.read_from_file('backup')
553        self.assertEqual('hello', state.get('ns', 'myvar'))
554
555
556    def test_read_updates_persistent_file(self):
557        state1 = base_job.job_state()
558        state1.set('ns', 'var1', 'value1')
559        state1.write_to_file('to_be_read')
560        state2 = base_job.job_state()
561        state2.set_backing_file('backing_file')
562        state2.set('ns', 'var2', 'value2')
563        state2.read_from_file('to_be_read')
564        state2.set_backing_file(None)
565        state3 = base_job.job_state()
566        state3.read_from_file('backing_file')
567        self.assertEqual('value1', state3.get('ns', 'var1'))
568        self.assertEqual('value2', state3.get('ns', 'var2'))
569
570
571    def test_read_without_merge(self):
572        state = base_job.job_state()
573        state.set('ns', 'myvar1', 'hello')
574        state.write_to_file('backup')
575        state.discard('ns', 'myvar1')
576        state.set('ns', 'myvar2', 'goodbye')
577        self.assertFalse(state.has('ns', 'myvar1'))
578        self.assertEqual('goodbye', state.get('ns', 'myvar2'))
579        state.read_from_file('backup', merge=False)
580        self.assertEqual('hello', state.get('ns', 'myvar1'))
581        self.assertFalse(state.has('ns', 'myvar2'))
582
583
584class test_job_state_set_backing_file(unittest.TestCase):
585    def setUp(self):
586        self.testdir = tempfile.mkdtemp(suffix='unittest')
587        self.original_wd = os.getcwd()
588        os.chdir(self.testdir)
589
590
591    def tearDown(self):
592        os.chdir(self.original_wd)
593        shutil.rmtree(self.testdir, ignore_errors=True)
594
595
596    def test_writes_to_file(self):
597        state = base_job.job_state()
598        state.set_backing_file('outfile1')
599        self.assert_(os.path.exists('outfile1'))
600
601
602    def test_set_backing_file_updates_existing_file(self):
603        state1 = base_job.job_state()
604        state1.set_backing_file('second_file')
605        state1.set('ns0', 'var1x', 100)
606        state1.set_backing_file(None)
607        state2 = base_job.job_state()
608        state2.set_backing_file('first_file')
609        state2.set('ns0', 'var0x', 0)
610        state2.set_backing_file('second_file')
611        state2.set_backing_file(None)
612        state3 = base_job.job_state()
613        state3.read_from_file('second_file')
614        self.assertEqual(0, state3.get('ns0', 'var0x'))
615        self.assertEqual(100, state3.get('ns0', 'var1x'))
616
617
618    def test_set_backing_file_does_not_overwrite_previous_backing_file(self):
619        state1 = base_job.job_state()
620        state1.set_backing_file('second_file')
621        state1.set('ns0', 'var1y', 10)
622        state1.set_backing_file(None)
623        state2 = base_job.job_state()
624        state2.set_backing_file('first_file')
625        state2.set('ns0', 'var0y', -10)
626        state2.set_backing_file('second_file')
627        state2.set_backing_file(None)
628        state3 = base_job.job_state()
629        state3.read_from_file('first_file')
630        self.assertEqual(-10, state3.get('ns0', 'var0y'))
631        self.assertRaises(KeyError, state3.get, 'ns0', 'var1y')
632
633
634    def test_writes_stop_after_backing_file_removed(self):
635        state = base_job.job_state()
636        state.set('ns', 'var1', 'value1')
637        state.set_backing_file('outfile2')
638        state.set_backing_file(None)
639        os.remove('outfile2')
640        state.set('n2', 'var2', 'value2')
641        self.assert_(not os.path.exists('outfile2'))
642
643
644    def test_written_files_can_be_reloaded(self):
645        state1 = base_job.job_state()
646        state1.set_backing_file('outfile3')
647        state1.set('n3', 'var1', 67)
648        state1.set_backing_file(None)
649        state2 = base_job.job_state()
650        self.assertRaises(KeyError, state2.get, 'n3', 'var1')
651        state2.set_backing_file('outfile3')
652        self.assertEqual(67, state2.get('n3', 'var1'))
653
654
655    def test_backing_file_overrides_in_memory_values(self):
656        state1 = base_job.job_state()
657        state1.set_backing_file('outfile4')
658        state1.set('n4', 'var1', 42)
659        state1.set_backing_file(None)
660        state2 = base_job.job_state()
661        state2.set('n4', 'var1', 430)
662        self.assertEqual(430, state2.get('n4', 'var1'))
663        state2.set_backing_file('outfile4')
664        self.assertEqual(42, state2.get('n4', 'var1'))
665
666
667    def test_backing_file_only_overrides_values_it_defines(self):
668        state1 = base_job.job_state()
669        state1.set_backing_file('outfile5')
670        state1.set('n5', 'var1', 123)
671        state1.set_backing_file(None)
672        state2 = base_job.job_state()
673        state2.set('n5', 'var2', 456)
674        state2.set_backing_file('outfile5')
675        self.assertEqual(123, state2.get('n5', 'var1'))
676        self.assertEqual(456, state2.get('n5', 'var2'))
677
678
679    def test_shared_backing_file_propagates_state_to_get(self):
680        state1 = base_job.job_state()
681        state1.set_backing_file('outfile6')
682        state2 = base_job.job_state()
683        state2.set_backing_file('outfile6')
684        self.assertRaises(KeyError, state1.get, 'n6', 'shared1')
685        self.assertRaises(KeyError, state2.get, 'n6', 'shared1')
686        state1.set('n6', 'shared1', 345)
687        self.assertEqual(345, state1.get('n6', 'shared1'))
688        self.assertEqual(345, state2.get('n6', 'shared1'))
689
690
691    def test_shared_backing_file_propagates_state_to_has(self):
692        state1 = base_job.job_state()
693        state1.set_backing_file('outfile7')
694        state2 = base_job.job_state()
695        state2.set_backing_file('outfile7')
696        self.assertFalse(state1.has('n6', 'shared2'))
697        self.assertFalse(state2.has('n6', 'shared2'))
698        state1.set('n6', 'shared2', 'hello')
699        self.assertTrue(state1.has('n6', 'shared2'))
700        self.assertTrue(state2.has('n6', 'shared2'))
701
702
703    def test_shared_backing_file_propagates_state_from_discard(self):
704        state1 = base_job.job_state()
705        state1.set_backing_file('outfile8')
706        state1.set('n6', 'shared3', 10000)
707        state2 = base_job.job_state()
708        state2.set_backing_file('outfile8')
709        self.assertEqual(10000, state1.get('n6', 'shared3'))
710        self.assertEqual(10000, state2.get('n6', 'shared3'))
711        state1.discard('n6', 'shared3')
712        self.assertRaises(KeyError, state1.get, 'n6', 'shared3')
713        self.assertRaises(KeyError, state2.get, 'n6', 'shared3')
714
715
716    def test_shared_backing_file_propagates_state_from_discard_namespace(self):
717        state1 = base_job.job_state()
718        state1.set_backing_file('outfile9')
719        state1.set('n7', 'shared4', -1)
720        state1.set('n7', 'shared5', -2)
721        state2 = base_job.job_state()
722        state2.set_backing_file('outfile9')
723        self.assertEqual(-1, state1.get('n7', 'shared4'))
724        self.assertEqual(-1, state2.get('n7', 'shared4'))
725        self.assertEqual(-2, state1.get('n7', 'shared5'))
726        self.assertEqual(-2, state2.get('n7', 'shared5'))
727        state1.discard_namespace('n7')
728        self.assertRaises(KeyError, state1.get, 'n7', 'shared4')
729        self.assertRaises(KeyError, state2.get, 'n7', 'shared4')
730        self.assertRaises(KeyError, state1.get, 'n7', 'shared5')
731        self.assertRaises(KeyError, state2.get, 'n7', 'shared5')
732
733
734class test_job_state_backing_file_locking(unittest.TestCase):
735    def setUp(self):
736        self.testdir = tempfile.mkdtemp(suffix='unittest')
737        self.original_wd = os.getcwd()
738        os.chdir(self.testdir)
739
740        # create a job_state object with stub read_* and write_* methods
741        # to check that a lock is always held during a call to them
742        ut_self = self
743        class mocked_job_state(base_job.job_state):
744            def read_from_file(self, file_path, merge=True):
745                if self._backing_file and file_path == self._backing_file:
746                    ut_self.assertNotEqual(None, self._backing_file_lock)
747                return super(mocked_job_state, self).read_from_file(
748                    file_path, merge=True)
749            def write_to_file(self, file_path):
750                if self._backing_file and file_path == self._backing_file:
751                    ut_self.assertNotEqual(None, self._backing_file_lock)
752                return super(mocked_job_state, self).write_to_file(file_path)
753        self.state = mocked_job_state()
754        self.state.set_backing_file('backing_file')
755
756
757    def tearDown(self):
758        os.chdir(self.original_wd)
759        shutil.rmtree(self.testdir, ignore_errors=True)
760
761
762    def test_set(self):
763        self.state.set('ns1', 'var1', 100)
764
765
766    def test_get_missing(self):
767        self.assertRaises(KeyError, self.state.get, 'ns2', 'var2')
768
769
770    def test_get_present(self):
771        self.state.set('ns3', 'var3', 333)
772        self.assertEqual(333, self.state.get('ns3', 'var3'))
773
774
775    def test_set_backing_file(self):
776        self.state.set_backing_file('some_other_file')
777
778
779    def test_has_missing(self):
780        self.assertFalse(self.state.has('ns4', 'var4'))
781
782
783    def test_has_present(self):
784        self.state.set('ns5', 'var5', 55555)
785        self.assertTrue(self.state.has('ns5', 'var5'))
786
787
788    def test_discard_missing(self):
789        self.state.discard('ns6', 'var6')
790
791
792    def test_discard_present(self):
793        self.state.set('ns7', 'var7', -777)
794        self.state.discard('ns7', 'var7')
795
796
797    def test_discard_missing_namespace(self):
798        self.state.discard_namespace('ns8')
799
800
801    def test_discard_present_namespace(self):
802        self.state.set('ns8', 'var8', 80)
803        self.state.set('ns8', 'var8.1', 81)
804        self.state.discard_namespace('ns8')
805
806
807    def test_disable_backing_file(self):
808        self.state.set_backing_file(None)
809
810
811    def test_change_backing_file(self):
812        self.state.set_backing_file('another_backing_file')
813
814
815    def test_read_from_a_non_backing_file(self):
816        state = base_job.job_state()
817        state.set('ns9', 'var9', 9999)
818        state.write_to_file('non_backing_file')
819        self.state.read_from_file('non_backing_file')
820
821
822    def test_write_to_a_non_backing_file(self):
823        self.state.write_to_file('non_backing_file')
824
825
826class test_job_state_property_factory(unittest.TestCase):
827    def setUp(self):
828        class job_stub(object):
829            pass
830        self.job_class = job_stub
831        self.job = job_stub()
832        self.state = base_job.job_state()
833        self.job.stateobj = self.state
834
835
836    def test_properties_are_readwrite(self):
837        self.job_class.testprop1 = base_job.job_state.property_factory(
838            'stateobj', 'testprop1', 1)
839        self.job.testprop1 = 'testvalue'
840        self.assertEqual('testvalue', self.job.testprop1)
841
842
843    def test_properties_use_default_if_not_initialized(self):
844        self.job_class.testprop2 = base_job.job_state.property_factory(
845            'stateobj', 'testprop2', 'abc123')
846        self.assertEqual('abc123', self.job.testprop2)
847
848
849    def test_properties_do_not_collisde(self):
850        self.job_class.testprop3 = base_job.job_state.property_factory(
851            'stateobj', 'testprop3', 2)
852        self.job_class.testprop4 = base_job.job_state.property_factory(
853            'stateobj', 'testprop4', 3)
854        self.job.testprop3 = 500
855        self.job.testprop4 = '1000'
856        self.assertEqual(500, self.job.testprop3)
857        self.assertEqual('1000', self.job.testprop4)
858
859
860    def test_properties_do_not_collide_across_different_state_objects(self):
861        self.job_class.testprop5 = base_job.job_state.property_factory(
862            'stateobj', 'testprop5', 55)
863        self.job.auxstateobj = base_job.job_state()
864        self.job_class.auxtestprop5 = base_job.job_state.property_factory(
865            'auxstateobj', 'testprop5', 600)
866        self.job.auxtestprop5 = 700
867        self.assertEqual(55, self.job.testprop5)
868        self.assertEqual(700, self.job.auxtestprop5)
869
870
871    def test_properties_do_not_collide_across_different_job_objects(self):
872        self.job_class.testprop6 = base_job.job_state.property_factory(
873            'stateobj', 'testprop6', 'defaultval')
874        job1 = self.job
875        job2 = self.job_class()
876        job2.stateobj = base_job.job_state()
877        job1.testprop6 = 'notdefaultval'
878        self.assertEqual('notdefaultval', job1.testprop6)
879        self.assertEqual('defaultval', job2.testprop6)
880        job2.testprop6 = 'job2val'
881        self.assertEqual('notdefaultval', job1.testprop6)
882        self.assertEqual('job2val', job2.testprop6)
883
884    def test_properties_in_different_namespaces_do_not_collide(self):
885        self.job_class.ns1 = base_job.job_state.property_factory(
886            'stateobj', 'attribute', 'default1', namespace='ns1')
887        self.job_class.ns2 = base_job.job_state.property_factory(
888            'stateobj', 'attribute', 'default2', namespace='ns2')
889        self.assertEqual('default1', self.job.ns1)
890        self.assertEqual('default2', self.job.ns2)
891        self.job.ns1 = 'notdefault'
892        self.job.ns2 = 'alsonotdefault'
893        self.assertEqual('notdefault', self.job.ns1)
894        self.assertEqual('alsonotdefault', self.job.ns2)
895
896
897class test_status_log_entry(unittest.TestCase):
898    def test_accepts_valid_status_code(self):
899        base_job.status_log_entry('GOOD', None, None, '', None)
900        base_job.status_log_entry('FAIL', None, None, '', None)
901        base_job.status_log_entry('ABORT', None, None, '', None)
902
903
904    def test_accepts_valid_start_status_code(self):
905        base_job.status_log_entry('START', None, None, '', None)
906
907
908    def test_accepts_valid_end_status_code(self):
909        base_job.status_log_entry('END GOOD', None, None, '', None)
910        base_job.status_log_entry('END FAIL', None, None, '', None)
911        base_job.status_log_entry('END ABORT', None, None, '', None)
912
913
914    def test_rejects_invalid_status_code(self):
915        self.assertRaises(ValueError, base_job.status_log_entry,
916                          'FAKE', None, None, '', None)
917
918
919    def test_rejects_invalid_start_status_code(self):
920        self.assertRaises(ValueError, base_job.status_log_entry,
921                          'START GOOD', None, None, '', None)
922        self.assertRaises(ValueError, base_job.status_log_entry,
923                          'START FAIL', None, None, '', None)
924        self.assertRaises(ValueError, base_job.status_log_entry,
925                          'START ABORT', None, None, '', None)
926        self.assertRaises(ValueError, base_job.status_log_entry,
927                          'START FAKE', None, None, '', None)
928
929
930    def test_rejects_invalid_end_status_code(self):
931        self.assertRaises(ValueError, base_job.status_log_entry,
932                          'END FAKE', None, None, '', None)
933
934
935    def test_accepts_valid_subdir(self):
936        base_job.status_log_entry('GOOD', 'subdir', None, '', None)
937        base_job.status_log_entry('FAIL', 'good.subdir', None, '', None)
938
939
940    def test_rejects_bad_subdir(self):
941        self.assertRaises(ValueError, base_job.status_log_entry,
942                          'GOOD', 'bad.subdir\t', None, '', None)
943        self.assertRaises(ValueError, base_job.status_log_entry,
944                          'GOOD', 'bad.subdir\t', None, '', None)
945        self.assertRaises(ValueError, base_job.status_log_entry,
946                          'GOOD', 'bad.subdir\t', None, '', None)
947        self.assertRaises(ValueError, base_job.status_log_entry,
948                          'GOOD', 'bad.subdir\t', None, '', None)
949        self.assertRaises(ValueError, base_job.status_log_entry,
950                          'GOOD', 'bad.subdir\t', None, '', None)
951
952
953    def test_accepts_valid_operation(self):
954        base_job.status_log_entry('GOOD', None, 'build', '', None)
955        base_job.status_log_entry('FAIL', None, 'clean', '', None)
956
957
958    def test_rejects_bad_operation(self):
959        self.assertRaises(ValueError, base_job.status_log_entry,
960                          'GOOD', None, 'bad.operation\n', '', None)
961        self.assertRaises(ValueError, base_job.status_log_entry,
962                          'GOOD', None, 'bad.\voperation', '', None)
963        self.assertRaises(ValueError, base_job.status_log_entry,
964                          'GOOD', None, 'bad.\foperation', '', None)
965        self.assertRaises(ValueError, base_job.status_log_entry,
966                          'GOOD', None, 'bad\r.operation', '', None)
967        self.assertRaises(ValueError, base_job.status_log_entry,
968                          'GOOD', None, '\tbad.operation', '', None)
969
970
971    def test_simple_message(self):
972        base_job.status_log_entry('ERROR', None, None, 'simple error message',
973                                  None)
974
975
976    def test_message_split_into_multiple_lines(self):
977        def make_entry(msg):
978            return base_job.status_log_entry('GOOD', None, None, msg, None)
979        base_job.status_log_entry('ABORT', None, None, 'first line\nsecond',
980                                  None)
981
982
983    def test_message_with_tabs(self):
984        base_job.status_log_entry('GOOD', None, None, '\tindent\tagain', None)
985
986
987    def test_message_with_custom_fields(self):
988        base_job.status_log_entry('GOOD', None, None, 'my message',
989                                  {'key1': 'blah', 'key2': 'blahblah'})
990
991
992    def assertRendered(self, rendered, status, subdir, operation, msg,
993                       extra_fields, timestamp):
994        parts = rendered.split('\t')
995        self.assertEqual(parts[0], status)
996        self.assertEqual(parts[1], subdir)
997        self.assertEqual(parts[2], operation)
998        self.assertEqual(parts[-1], msg)
999        fields = dict(f.split('=', 1) for f in parts[3:-1])
1000        self.assertEqual(int(fields['timestamp']), timestamp)
1001        self.assert_('localtime' in fields)  # too flaky to do an exact check
1002        del fields['timestamp']
1003        del fields['localtime']
1004        self.assertEqual(fields, extra_fields)
1005
1006
1007    def test_base_render(self):
1008        entry = base_job.status_log_entry('GOOD', None, None, 'message1', None,
1009                                          timestamp=1)
1010        self.assertRendered(entry.render(), 'GOOD', '----', '----', 'message1',
1011                            {}, 1)
1012
1013
1014    def test_subdir_render(self):
1015        entry = base_job.status_log_entry('FAIL', 'sub', None, 'message2', None,
1016                                          timestamp=2)
1017        self.assertRendered(entry.render(), 'FAIL', 'sub', '----', 'message2',
1018                            {}, 2)
1019
1020
1021    def test_operation_render(self):
1022        entry = base_job.status_log_entry('ABORT', None, 'myop', 'message3',
1023                                          None, timestamp=4)
1024        self.assertRendered(entry.render(), 'ABORT', '----', 'myop', 'message3',
1025                            {}, 4)
1026
1027
1028    def test_fields_render(self):
1029        custom_fields = {'custom1': 'foo', 'custom2': 'bar'}
1030        entry = base_job.status_log_entry('WARN', None, None, 'message4',
1031                                          custom_fields, timestamp=8)
1032        self.assertRendered(entry.render(), 'WARN', '----', '----', 'message4',
1033                            custom_fields, 8)
1034
1035
1036    def assertEntryEqual(self, lhs, rhs):
1037        self.assertEqual(
1038          (lhs.status_code, lhs.subdir, lhs.operation, lhs.fields, lhs.message),
1039          (rhs.status_code, rhs.subdir, rhs.operation, rhs.fields, rhs.message))
1040
1041
1042    def test_base_parse(self):
1043        entry = base_job.status_log_entry(
1044            'GOOD', None, None, 'message', {'field1': 'x', 'field2': 'y'},
1045            timestamp=16)
1046        parsed_entry = base_job.status_log_entry.parse(
1047            'GOOD\t----\t----\tfield1=x\tfield2=y\ttimestamp=16\tmessage\n')
1048        self.assertEntryEqual(entry, parsed_entry)
1049
1050
1051    def test_subdir_parse(self):
1052        entry = base_job.status_log_entry(
1053            'FAIL', 'sub', None, 'message', {'field1': 'x', 'field2': 'y'},
1054            timestamp=32)
1055        parsed_entry = base_job.status_log_entry.parse(
1056            'FAIL\tsub\t----\tfield1=x\tfield2=y\ttimestamp=32\tmessage\n')
1057        self.assertEntryEqual(entry, parsed_entry)
1058
1059
1060    def test_operation_parse(self):
1061        entry = base_job.status_log_entry(
1062            'ABORT', None, 'myop', 'message', {'field1': 'x', 'field2': 'y'},
1063            timestamp=64)
1064        parsed_entry = base_job.status_log_entry.parse(
1065            'ABORT\t----\tmyop\tfield1=x\tfield2=y\ttimestamp=64\tmessage\n')
1066        self.assertEntryEqual(entry, parsed_entry)
1067
1068
1069    def test_extra_lines_parse(self):
1070        parsed_entry = base_job.status_log_entry.parse(
1071            '  This is a non-status line, line in a traceback\n')
1072        self.assertEqual(None, parsed_entry)
1073
1074
1075class test_status_logger(unittest.TestCase):
1076    def setUp(self):
1077        self.testdir = tempfile.mkdtemp(suffix='unittest')
1078        self.original_wd = os.getcwd()
1079        os.chdir(self.testdir)
1080
1081        class stub_job(object):
1082            resultdir = self.testdir
1083        self.job = stub_job()  # need to hold a reference to the job
1084        class stub_indenter(object):
1085            def __init__(self):
1086                self.indent = 0
1087            def increment(self):
1088                self.indent += 1
1089            def decrement(self):
1090                self.indent -= 1
1091        self.indenter = stub_indenter()
1092        self.logger = base_job.status_logger(self.job, self.indenter)
1093
1094
1095    def make_dummy_entry(self, rendered_text, start=False, end=False,
1096                         subdir=None):
1097        """Helper to make a dummy status log entry with custom rendered text.
1098
1099        Helpful when validating the logging since it lets the test control
1100        the rendered text and so it doesn't depend on the exact formatting
1101        of a "real" status log entry.
1102
1103        @param rendred_text: The value to return when rendering the entry.
1104        @param start: An optional value indicating if this should be the start
1105            of a nested group.
1106        @param end: An optional value indicating if this should be the end
1107            of a nested group.
1108        @param subdir: An optional value to use for the entry subdir field.
1109
1110        @return: A dummy status log entry object with the given subdir field
1111            and a render implementation that returns rendered_text.
1112        """
1113        assert not start or not end  # real entries would never be both
1114        class dummy_entry(object):
1115            def is_start(self):
1116                return start
1117            def is_end(self):
1118                return end
1119            def render(self):
1120                return rendered_text
1121        entry = dummy_entry()
1122        entry.subdir = subdir
1123        return entry
1124
1125
1126    def test_render_includes_indent(self):
1127        entry = self.make_dummy_entry('LINE0')
1128        self.assertEqual('LINE0', self.logger.render_entry(entry))
1129        self.indenter.increment()
1130        self.indenter.increment()
1131        self.assertEqual('\t\tLINE0', self.logger.render_entry(entry))
1132
1133
1134    def test_render_handles_start(self):
1135        entry = self.make_dummy_entry('LINE10', start=True)
1136        self.indenter.increment()
1137        self.assertEqual('\tLINE10', self.logger.render_entry(entry))
1138
1139
1140    def test_render_handles_end(self):
1141        entry = self.make_dummy_entry('LINE20', end=True)
1142        self.indenter.increment()
1143        self.indenter.increment()
1144        self.indenter.increment()
1145        self.assertEqual('\t\tLINE20', self.logger.render_entry(entry))
1146
1147
1148    def test_writes_toplevel_log(self):
1149        entries = [self.make_dummy_entry('LINE%d' % x) for x in xrange(3)]
1150        for entry in entries:
1151            self.logger.record_entry(entry)
1152        self.assertEqual('LINE0\nLINE1\nLINE2\n', open('status').read())
1153
1154
1155    def test_uses_given_filenames(self):
1156        os.mkdir('sub')
1157        self.logger = base_job.status_logger(self.job, self.indenter,
1158                                             global_filename='global.log',
1159                                             subdir_filename='subdir.log')
1160        self.logger.record_entry(self.make_dummy_entry('LINE1', subdir='sub'))
1161        self.logger.record_entry(self.make_dummy_entry('LINE2', subdir='sub'))
1162        self.logger.record_entry(self.make_dummy_entry('LINE3'))
1163
1164        self.assertEqual('LINE1\nLINE2\nLINE3\n', open('global.log').read())
1165        self.assertEqual('LINE1\nLINE2\n', open('sub/subdir.log').read())
1166
1167        self.assertFalse(os.path.exists('status'))
1168        self.assertFalse(os.path.exists('sub/status'))
1169        self.assertFalse(os.path.exists('subdir.log'))
1170        self.assertFalse(os.path.exists('sub/global.log'))
1171
1172
1173    def test_filenames_are_mutable(self):
1174        os.mkdir('sub2')
1175        self.logger = base_job.status_logger(self.job, self.indenter,
1176                                             global_filename='global.log',
1177                                             subdir_filename='subdir.log')
1178        self.logger.record_entry(self.make_dummy_entry('LINE1', subdir='sub2'))
1179        self.logger.record_entry(self.make_dummy_entry('LINE2'))
1180        self.logger.global_filename = 'global.log2'
1181        self.logger.subdir_filename = 'subdir.log2'
1182        self.logger.record_entry(self.make_dummy_entry('LINE3', subdir='sub2'))
1183        self.logger.record_entry(self.make_dummy_entry('LINE4'))
1184
1185        self.assertEqual('LINE1\nLINE2\n', open('global.log').read())
1186        self.assertEqual('LINE1\n', open('sub2/subdir.log').read())
1187        self.assertEqual('LINE3\nLINE4\n', open('global.log2').read())
1188        self.assertEqual('LINE3\n', open('sub2/subdir.log2').read())
1189
1190
1191    def test_writes_subdir_logs(self):
1192        os.mkdir('abc')
1193        os.mkdir('123')
1194        self.logger.record_entry(self.make_dummy_entry('LINE1'))
1195        self.logger.record_entry(self.make_dummy_entry('LINE2', subdir='abc'))
1196        self.logger.record_entry(self.make_dummy_entry('LINE3', subdir='abc'))
1197        self.logger.record_entry(self.make_dummy_entry('LINE4', subdir='123'))
1198
1199        self.assertEqual('LINE1\nLINE2\nLINE3\nLINE4\n', open('status').read())
1200        self.assertEqual('LINE2\nLINE3\n', open('abc/status').read())
1201        self.assertEqual('LINE4\n', open('123/status').read())
1202
1203
1204    def test_writes_no_subdir_when_disabled(self):
1205        os.mkdir('sub')
1206        self.logger.record_entry(self.make_dummy_entry('LINE1'))
1207        self.logger.record_entry(self.make_dummy_entry('LINE2', subdir='sub'))
1208        self.logger.record_entry(self.make_dummy_entry(
1209            'LINE3', subdir='sub_nowrite'), log_in_subdir=False)
1210        self.logger.record_entry(self.make_dummy_entry('LINE4', subdir='sub'))
1211
1212        self.assertEqual('LINE1\nLINE2\nLINE3\nLINE4\n', open('status').read())
1213        self.assertEqual('LINE2\nLINE4\n', open('sub/status').read())
1214        self.assert_(not os.path.exists('sub_nowrite/status'))
1215
1216
1217    def test_indentation(self):
1218        self.logger.record_entry(self.make_dummy_entry('LINE1', start=True))
1219        self.logger.record_entry(self.make_dummy_entry('LINE2'))
1220        self.logger.record_entry(self.make_dummy_entry('LINE3', start=True))
1221        self.logger.record_entry(self.make_dummy_entry('LINE4'))
1222        self.logger.record_entry(self.make_dummy_entry('LINE5'))
1223        self.logger.record_entry(self.make_dummy_entry('LINE6', end=True))
1224        self.logger.record_entry(self.make_dummy_entry('LINE7', end=True))
1225        self.logger.record_entry(self.make_dummy_entry('LINE8'))
1226
1227        expected_log = ('LINE1\n\tLINE2\n\tLINE3\n\t\tLINE4\n\t\tLINE5\n'
1228                        '\tLINE6\nLINE7\nLINE8\n')
1229        self.assertEqual(expected_log, open('status').read())
1230
1231
1232    def test_multiline_indent(self):
1233        self.logger.record_entry(self.make_dummy_entry('LINE1\n  blah\n'))
1234        self.logger.record_entry(self.make_dummy_entry('LINE2', start=True))
1235        self.logger.record_entry(
1236            self.make_dummy_entry('LINE3\n  blah\n  two\n'))
1237        self.logger.record_entry(self.make_dummy_entry('LINE4', end=True))
1238
1239        expected_log = ('LINE1\n  blah\nLINE2\n'
1240                        '\tLINE3\n  blah\n  two\nLINE4\n')
1241        self.assertEqual(expected_log, open('status').read())
1242
1243
1244    def test_hook_is_called(self):
1245        entries = [self.make_dummy_entry('LINE%d' % x) for x in xrange(5)]
1246        recorded_entries = []
1247        def hook(entry):
1248            recorded_entries.append(entry)
1249        self.logger = base_job.status_logger(self.job, self.indenter,
1250                                             record_hook=hook)
1251        for entry in entries:
1252            self.logger.record_entry(entry)
1253        self.assertEqual(entries, recorded_entries)
1254
1255
1256    def tearDown(self):
1257        os.chdir(self.original_wd)
1258        shutil.rmtree(self.testdir, ignore_errors=True)
1259
1260
1261class test_job_tags(unittest.TestCase):
1262    def setUp(self):
1263        class stub_job(base_job.base_job):
1264            _job_directory = stub_job_directory
1265            @classmethod
1266            def _find_base_directories(cls):
1267                return '/autodir', '/autodir/client', '/autodir/server'
1268            def _find_resultdir(self):
1269                return '/autodir/results'
1270        self.job = stub_job()
1271
1272
1273    def test_default_with_no_args_means_no_tags(self):
1274        self.assertEqual(('testname', 'testname', ''),
1275                         self.job._build_tagged_test_name('testname', {}))
1276        self.assertEqual(('othername', 'othername', ''),
1277                         self.job._build_tagged_test_name('othername', {}))
1278
1279
1280    def test_tag_argument_appended(self):
1281        self.assertEqual(
1282            ('test1.mytag', 'test1.mytag', 'mytag'),
1283            self.job._build_tagged_test_name('test1', {'tag': 'mytag'}))
1284
1285
1286    def test_turning_on_use_sequence_adds_sequence_tags(self):
1287        self.job.use_sequence_number = True
1288        self.assertEqual(
1289            ('test2._01_', 'test2._01_', '_01_'),
1290            self.job._build_tagged_test_name('test2', {}))
1291        self.assertEqual(
1292            ('test2._02_', 'test2._02_', '_02_'),
1293            self.job._build_tagged_test_name('test2', {}))
1294        self.assertEqual(
1295            ('test3._03_', 'test3._03_', '_03_'),
1296            self.job._build_tagged_test_name('test3', {}))
1297
1298
1299    def test_adding_automatic_test_tag_automatically_tags(self):
1300        self.job.automatic_test_tag = 'autotag'
1301        self.assertEqual(
1302            ('test4.autotag', 'test4.autotag', 'autotag'),
1303            self.job._build_tagged_test_name('test4', {}))
1304
1305
1306    def test_none_automatic_test_tag_turns_off_tagging(self):
1307        self.job.automatic_test_tag = 'autotag'
1308        self.assertEqual(
1309            ('test5.autotag', 'test5.autotag', 'autotag'),
1310            self.job._build_tagged_test_name('test5', {}))
1311        self.job.automatic_test_tag = None
1312        self.assertEqual(
1313            ('test5', 'test5', ''),
1314            self.job._build_tagged_test_name('test5', {}))
1315
1316
1317    def test_empty_automatic_test_tag_turns_off_tagging(self):
1318        self.job.automatic_test_tag = 'autotag'
1319        self.assertEqual(
1320            ('test6.autotag', 'test6.autotag', 'autotag'),
1321            self.job._build_tagged_test_name('test6', {}))
1322        self.job.automatic_test_tag = ''
1323        self.assertEqual(
1324            ('test6', 'test6', ''),
1325            self.job._build_tagged_test_name('test6', {}))
1326
1327
1328    def test_subdir_tag_modifies_subdir_and_tag_only(self):
1329        self.assertEqual(
1330            ('test7', 'test7.subdirtag', 'subdirtag'),
1331            self.job._build_tagged_test_name('test7',
1332                                             {'subdir_tag': 'subdirtag'}))
1333
1334
1335    def test_all_tag_components_together(self):
1336        self.job.use_sequence_number = True
1337        self.job.automatic_test_tag = 'auto'
1338        expected = ('test8.tag._01_.auto',
1339                    'test8.tag._01_.auto.subdir',
1340                    'tag._01_.auto.subdir')
1341        actual = self.job._build_tagged_test_name(
1342            'test8', {'tag': 'tag', 'subdir_tag': 'subdir'})
1343        self.assertEqual(expected, actual)
1344
1345
1346    def test_subtest_with_master_test_path_and_subdir(self):
1347        self.assertEqual(
1348            ('test9', 'subtestdir/test9.subdirtag', 'subdirtag'),
1349            self.job._build_tagged_test_name('test9',
1350                                             {'master_testpath': 'subtestdir',
1351                                              'subdir_tag': 'subdirtag'}))
1352
1353
1354    def test_subtest_all_tag_components_together_subdir(self):
1355        self.job.use_sequence_number = True
1356        self.job.automatic_test_tag = 'auto'
1357        expected = ('test10.tag._01_.auto',
1358                    'subtestdir/test10.tag._01_.auto.subdir',
1359                    'tag._01_.auto.subdir')
1360        actual = self.job._build_tagged_test_name(
1361            'test10', {'tag': 'tag', 'subdir_tag': 'subdir',
1362                       'master_testpath': 'subtestdir'})
1363        self.assertEqual(expected, actual)
1364
1365
1366class test_make_outputdir(unittest.TestCase):
1367    def setUp(self):
1368        self.resultdir = tempfile.mkdtemp(suffix='unittest')
1369        class stub_job(base_job.base_job):
1370            @classmethod
1371            def _find_base_directories(cls):
1372                return '/autodir', '/autodir/client', '/autodir/server'
1373            @classmethod
1374            def _find_resultdir(cls):
1375                return self.resultdir
1376
1377        # stub out _job_directory for creation only
1378        stub_job._job_directory = stub_job_directory
1379        self.job = stub_job()
1380        del stub_job._job_directory
1381
1382        # stub out logging.exception
1383        self.original_exception = logging.exception
1384        logging.exception = lambda *args, **dargs: None
1385
1386        self.original_wd = os.getcwd()
1387        os.chdir(self.resultdir)
1388
1389
1390    def tearDown(self):
1391        logging.exception = self.original_exception
1392        os.chdir(self.original_wd)
1393        shutil.rmtree(self.resultdir, ignore_errors=True)
1394
1395
1396    def test_raises_test_error_if_outputdir_exists(self):
1397        os.mkdir('subdir1')
1398        self.assert_(os.path.exists('subdir1'))
1399        self.assertRaises(error.TestError, self.job._make_test_outputdir,
1400                          'subdir1')
1401
1402
1403    def test_raises_test_error_if_outputdir_uncreatable(self):
1404        os.chmod(self.resultdir, stat.S_IRUSR | stat.S_IXUSR)
1405        self.assert_(not os.path.exists('subdir2'))
1406        self.assertRaises(OSError, os.mkdir, 'subdir2')
1407        self.assertRaises(error.TestError, self.job._make_test_outputdir,
1408                          'subdir2')
1409        self.assert_(not os.path.exists('subdir2'))
1410
1411
1412    def test_creates_writable_directory(self):
1413        self.assert_(not os.path.exists('subdir3'))
1414        self.job._make_test_outputdir('subdir3')
1415        self.assert_(os.path.isdir('subdir3'))
1416
1417        # we can write to the directory afterwards
1418        self.assert_(not os.path.exists('subdir3/testfile'))
1419        open('subdir3/testfile', 'w').close()
1420        self.assert_(os.path.isfile('subdir3/testfile'))
1421
1422
1423if __name__ == "__main__":
1424    unittest.main()
1425