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