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 5from __future__ import absolute_import 6from __future__ import division 7from __future__ import print_function 8 9import collections 10import contextlib 11import logging 12import os 13import signal 14import socket 15import sys 16 17import mock 18import pytest 19import subprocess32 20 21from lucifer import leasing 22 23logger = logging.getLogger(__name__) 24 25# 9999-01-01T00:00:00+00:00 26_THE_END = 253370764800 27 28 29def 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 42def 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 51def 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 61def 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 71def 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 79def 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 87def 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 99def 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 111def 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 124def 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 138def 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 148def _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 177def _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 200def _leases_dict(jobdir): 201 """Convenience method for tests.""" 202 return {lease.id: lease for lease 203 in leasing.leases_iter(jobdir)} 204 205 206def _make_lease(tmpdir, job_id): 207 return _make_lease_file(str(tmpdir), job_id) 208 209 210def _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 222class _TestError(Exception): 223 """Error for tests.""" 224