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