Skip to content

Commit

Permalink
Merge pull request #27 from pytest-dev/feature/zc-lockfile
Browse files Browse the repository at this point in the history
Rely on zc.lockfile for file locking behavior
  • Loading branch information
jaraco committed Nov 6, 2019
2 parents 985a760 + 1be24f5 commit 69c3f66
Show file tree
Hide file tree
Showing 8 changed files with 34 additions and 83 deletions.
9 changes: 0 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,6 @@ python: "2.7"
matrix:
include:
- env: TOXENV=linters
- env: TOXENV=py27-pytest30
- env: TOXENV=py27-pytest31
- env: TOXENV=py27-pytest32
- env: TOXENV=py27-pytest33
- env: TOXENV=py27-pytest34
- env: TOXENV=py27-pytest35
- env: TOXENV=py27-pytest36
- env: TOXENV=py27-pytest37
- env: TOXENV=py27-pytest38
- env: TOXENV=py27-pytest39
- env: TOXENV=py27-pytest310
- env: TOXENV=py27-pytestlatest
Expand Down
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

2.0.0
-----

- #23: Rely on ``zc.lockfile`` for lockfile behavior.
- #28: Fixtures now supports later versions of mysql and no longer
support versions of mysql prior to ``mysql --initialize`` support.
- #29: Fix issues with later versions of mysql where ``mysql_defaults_file``
fixture would prevent startup of mysql.
- Fixed issue in test suite where mysql fixture was not tested.
- Removed ``pytest_services.locks.lock_file``.

1.3.1
-----

Expand Down
59 changes: 7 additions & 52 deletions pytest_services/locks.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,33 @@
"""Fixtures for supporting a distributed test run."""
import contextlib
import errno
import fcntl
import json
import os
import socket
import time

import pytest
import zc.lockfile

marker = object()


def lock_file(filename, content, operation):
"""Lock given file.
:param filename: full path to the lockfile
:param content: content string to write to the lockfile after successful lock
:param operation: os operation to use for lock
:return: file object of opened locked file. Can be used to write content to it
"""
try:
handle = os.fdopen(os.open(filename, os.O_RDWR | os.O_CREAT, 0o666), 'r+')
except OSError as e:
if e.errno == errno.EACCES:
raise Exception('Failed to open/create file. Check permissions on containing folder')
raise
fcntl.flock(handle, operation)
if content:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
def try_remove(filename):
try:
os.chmod(filename, 0o666)
os.unlink(filename)
except OSError:
pass
return handle


def unlock_file(filename, handle, remove=True):
"""Unlock given file."""
fcntl.flock(handle, fcntl.LOCK_UN)
handle.close()
if remove:
try:
os.unlink(filename)
except OSError:
pass


@contextlib.contextmanager
def file_lock(filename, operation=fcntl.LOCK_EX, remove=True, timeout=20):
def file_lock(filename, remove=True, timeout=20):
"""A lock that is shared across processes.
:param filename: the name of the file that will be locked.
:pram operation: the lock operation.
"""
# http://bservices_log.vmfarms.com/2011/03/cross-process-locking-and.html
times = 0
while True:
try:
handle = lock_file(filename, None, operation)
break
except IOError:
if times > timeout:
raise Exception('Not able to aquire lock file {0} in {1}'.format(filename, timeout))
time.sleep(0.5)
times += 1
with contextlib.closing(zc.lockfile.SimpleLockFile(filename)) as lockfile:
yield lockfile._fp

try:
yield handle
finally:
unlock_file(filename, handle, remove=remove)
remove and try_remove(filename)


def unlock_resource(name, resource, lock_dir, services_log):
Expand Down
17 changes: 10 additions & 7 deletions pytest_services/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@


@pytest.fixture(scope='session')
def mysql_defaults_file(run_services, mysql_data_dir, memory_temp_dir):
def mysql_defaults_file(
run_services, tmp_path_factory, memory_temp_dir, request):
"""MySQL defaults file."""
if run_services:
defaults_path = os.path.join(mysql_data_dir, 'defaults.cnf')
cfg = tmp_path_factory.mktemp(request.session.name)
defaults_path = str(cfg / 'defaults.cnf')

with open(defaults_path, 'w+') as fd:
fd.write("""
Expand Down Expand Up @@ -47,14 +49,15 @@ def mysql_system_database(
):
"""Install database to given path."""
if run_services:
mysql_install_db = find_executable('mysql_install_db')
assert mysql_install_db, 'You have to install mysql_install_db script.'
mysqld = find_executable('mysqld')
assert mysqld, 'You have to install mysqld script.'

try:
services_log.debug('Starting mysql_install_db.')
services_log.debug('Starting mysqld.')
check_output([
mysql_install_db,
mysqld,
'--defaults-file={0}'.format(mysql_defaults_file),
'--initialize-insecure',
'--datadir={0}'.format(mysql_data_dir),
'--basedir={0}'.format(mysql_base_dir),
'--user={0}'.format(os.environ['USER'])
Expand All @@ -65,7 +68,7 @@ def mysql_system_database(
'Please ensure you disabled apparmor for /run/shm/** or for whole mysql'.format(e=e))
raise
finally:
services_log.debug('mysql_install_db was executed.')
services_log.debug('mysqld was executed.')


@pytest.fixture(scope='session')
Expand Down
3 changes: 1 addition & 2 deletions pytest_services/xvfb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Fixtures for the GUI environment."""
import os
import fcntl
import socket
import re
try:
Expand Down Expand Up @@ -71,7 +70,7 @@ def xvfb(request, run_services, xvfb_display, lock_dir, xvfb_resolution, watcher
listen_args = []

with file_lock(os.path.join(lock_dir, 'xvfb_{0}.lock'.format(xvfb_display)),
operation=fcntl.LOCK_EX | fcntl.LOCK_NB):
):

def checker():
try:
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'requests',
'psutil',
'pytest',
'zc.lockfile >= 2.0',
]

PY2 = sys.version_info[0] < 3
Expand Down
4 changes: 2 additions & 2 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ def test_memcached(request, memcached, memcached_socket):
assert mc.get('some') is None


def test_mysql(mysql, mysql_connection):
def test_mysql(mysql, mysql_connection, mysql_socket):
"""Test mysql service."""
conn = MySQLdb.connect(user='root')
conn = MySQLdb.connect(user='root', unix_socket=mysql_socket)
assert conn


Expand Down
13 changes: 2 additions & 11 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,18 @@
distshare={homedir}/.tox/distshare
envlist=
linters
py27-pytest{30,31,32,33,34,35,36,37,38,39,310,latest}
py27-pytest{39,310,latest}
py{34,35,36}-pytestlatest
py27-pytestlatest-xdist
py27-pytestlatest-coveralls
skip_missing_interpreters = true

[testenv]
commands= py.test tests --junitxml={envlogdir}/junit-{envname}.xml
commands= py.test tests --junitxml={envlogdir}/junit-{envname}.xml {posargs}
deps =
pytestlatest: pytest
pytest310: pytest~=3.10.0
pytest39: pytest~=3.9.0
pytest38: pytest~=3.8.0
pytest37: pytest~=3.7.0
pytest36: pytest~=3.6.0
pytest35: pytest~=3.5.0
pytest34: pytest~=3.4.0
pytest33: pytest~=3.3.0
pytest32: pytest~=3.2.0
pytest31: pytest~=3.1.0
pytest30: pytest~=3.0.0
xdist: pytest-xdist
-r{toxinidir}/requirements-testing.txt
passenv = DISPLAY COVERALLS_REPO_TOKEN USER TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
Expand Down

0 comments on commit 69c3f66

Please sign in to comment.