Home | History | Annotate | Download | only in lucifer
      1 # Copyright 2017 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 from __future__ import absolute_import
      6 from __future__ import division
      7 from __future__ import print_function
      8 
      9 import collections
     10 import contextlib
     11 import logging
     12 import os
     13 import signal
     14 import socket
     15 import sys
     16 
     17 import mock
     18 import pytest
     19 import subprocess32
     20 
     21 from lucifer import leasing
     22 
     23 logger = logging.getLogger(__name__)
     24 
     25 # 9999-01-01T00:00:00+00:00
     26 _THE_END = 253370764800
     27 
     28 
     29 def test_obtain_lease(tmpdir):
     30     """Test obtain_lease.
     31 
     32     Provides basic test coverage metrics.  The slower subprocess tests
     33     provide better functional coverage.
     34     """
     35     path = _make_lease(tmpdir, 124)
     36     with leasing.obtain_lease(path):
     37         pass
     38     assert not os.path.exists(path)
     39 
     40 
     41 @pytest.mark.slow
     42 def test_obtain_lease_succesfully_removes_file(tmpdir):
     43     """Test obtain_lease cleans up lease file if successful."""
     44     path = _make_lease(tmpdir, 124)
     45     with _obtain_lease(path) as lease_proc:
     46         lease_proc.finish()
     47     assert not os.path.exists(path)
     48 
     49 
     50 @pytest.mark.slow
     51 def test_obtain_lease_with_error_removes_files(tmpdir):
     52     """Test obtain_lease removes file if it errors."""
     53     path = _make_lease(tmpdir, 124)
     54     with _obtain_lease(path) as lease_proc:
     55         lease_proc.proc.send_signal(signal.SIGINT)
     56         lease_proc.proc.wait()
     57     assert not os.path.exists(path)
     58 
     59 
     60 @pytest.mark.slow
     61 def test_Lease__expired(tmpdir, end_time):
     62     """Test get_expired_leases()."""
     63     _make_lease(tmpdir, 123)
     64     path = _make_lease(tmpdir, 124)
     65     with _obtain_lease(path):
     66         leases = _leases_dict(str(tmpdir))
     67         assert leases[123].expired()
     68         assert not leases[124].expired()
     69 
     70 
     71 def test_unlocked_fresh_leases_are_not_expired(tmpdir):
     72     """Test get_expired_leases()."""
     73     path = _make_lease(tmpdir, 123)
     74     os.utime(path, (_THE_END, _THE_END))
     75     leases = _leases_dict(str(tmpdir))
     76     assert not leases[123].expired()
     77 
     78 
     79 def test_leases_iter_with_sock_files(tmpdir):
     80     """Test leases_iter() ignores sock files."""
     81     _make_lease(tmpdir, 123)
     82     tmpdir.join('124.sock').write('')
     83     leases = _leases_dict(str(tmpdir))
     84     assert 124 not in leases
     85 
     86 
     87 def test_Job_cleanup(tmpdir):
     88     """Test Job.cleanup()."""
     89     lease_path = _make_lease(tmpdir, 123)
     90     tmpdir.join('123.sock').write('')
     91     sock_path = str(tmpdir.join('123.sock'))
     92     for job in leasing.leases_iter(str(tmpdir)):
     93         logger.debug('Cleaning up %r', job)
     94         job.cleanup()
     95     assert not os.path.exists(lease_path)
     96     assert not os.path.exists(sock_path)
     97 
     98 
     99 def test_Job_cleanup_does_not_raise_on_error(tmpdir):
    100     """Test Job.cleanup()."""
    101     lease_path = _make_lease(tmpdir, 123)
    102     tmpdir.join('123.sock').write('')
    103     sock_path = str(tmpdir.join('123.sock'))
    104     for job in leasing.leases_iter(str(tmpdir)):
    105         os.unlink(lease_path)
    106         os.unlink(sock_path)
    107         job.cleanup()
    108 
    109 
    110 @pytest.mark.slow
    111 def test_Job_abort(tmpdir):
    112     """Test Job.abort()."""
    113     _make_lease(tmpdir, 123)
    114     with _abort_socket(tmpdir, 123) as proc:
    115         expired = list(leasing.leases_iter(str(tmpdir)))
    116         assert len(expired) > 0
    117         for job in expired:
    118             job.abort()
    119         proc.wait()
    120         assert proc.returncode == 0
    121 
    122 
    123 @pytest.mark.slow
    124 def test_Job_abort_with_closed_socket(tmpdir):
    125     """Test Job.abort() with closed socket."""
    126     _make_lease(tmpdir, 123)
    127     with _abort_socket(tmpdir, 123) as proc:
    128         proc.terminate()
    129         proc.wait()
    130         expired = list(leasing.leases_iter(str(tmpdir)))
    131         assert len(expired) > 0
    132         for job in expired:
    133             with pytest.raises(socket.error):
    134                 job.abort()
    135 
    136 
    137 @pytest.fixture
    138 def end_time():
    139     """Mock out time.time to return a time in the future."""
    140     with mock.patch('time.time', return_value=_THE_END) as t:
    141         yield t
    142 
    143 
    144 _LeaseProc = collections.namedtuple('_LeaseProc', 'finish proc')
    145 
    146 
    147 @contextlib.contextmanager
    148 def _obtain_lease(path):
    149     """Lock a lease file.
    150 
    151     Yields a _LeaseProc.  finish is a function that can be called to
    152     finish the process normally.  proc is a Popen instance.
    153 
    154     This uses a slow subprocess; any test that uses this should be
    155     marked slow.
    156     """
    157     with subprocess32.Popen(
    158             [sys.executable, '-um',
    159              'lucifer.cmd.test.obtain_lease', path],
    160             stdin=subprocess32.PIPE,
    161             stdout=subprocess32.PIPE) as proc:
    162         # Wait for lock grab.
    163         proc.stdout.readline()
    164 
    165         def finish():
    166             """Finish lease process normally."""
    167             proc.stdin.write('\n')
    168             # Wait for lease release.
    169             proc.stdout.readline()
    170         try:
    171             yield _LeaseProc(finish, proc)
    172         finally:
    173             proc.terminate()
    174 
    175 
    176 @contextlib.contextmanager
    177 def _abort_socket(tmpdir, job_id):
    178     """Open a testing abort socket and listener for a job.
    179 
    180     As a context manager, returns the Popen instance for the listener
    181     process when entering.
    182 
    183     This uses a slow subprocess; any test that uses this should be
    184     marked slow.
    185     """
    186     path = os.path.join(str(tmpdir), '%d.sock' % job_id)
    187     logger.debug('Making abort socket at %s', path)
    188     with subprocess32.Popen(
    189             [sys.executable, '-um',
    190              'lucifer.cmd.test.abort_socket', path],
    191             stdout=subprocess32.PIPE) as proc:
    192         # Wait for socket bind.
    193         proc.stdout.readline()
    194         try:
    195             yield proc
    196         finally:
    197             proc.terminate()
    198 
    199 
    200 def _leases_dict(jobdir):
    201     """Convenience method for tests."""
    202     return {lease.id: lease for lease
    203             in leasing.leases_iter(jobdir)}
    204 
    205 
    206 def _make_lease(tmpdir, job_id):
    207     return _make_lease_file(str(tmpdir), job_id)
    208 
    209 
    210 def _make_lease_file(jobdir, job_id):
    211     """Make lease file corresponding to a job.
    212 
    213     @param jobdir: job lease file directory
    214     @param job_id: Job ID
    215     """
    216     path = os.path.join(jobdir, str(job_id))
    217     with open(path, 'w'):
    218         pass
    219     return path
    220 
    221 
    222 class _TestError(Exception):
    223     """Error for tests."""
    224