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