Skip to content

Commit 8f5184c

Browse files
authored
Merge pull request python-ldap#141 – Make testing on non-Linux platforms easier
python-ldap#141
2 parents 9fb9338 + 02915b3 commit 8f5184c

File tree

7 files changed

+169
-42
lines changed

7 files changed

+169
-42
lines changed

Lib/ldap/compat.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Compatibility wrappers for Py2/Py3."""
22

33
import sys
4+
import os
45

56
if sys.version_info[0] < 3:
67
from UserDict import UserDict, IterableUserDict
@@ -41,3 +42,72 @@ def reraise(exc_type, exc_value, exc_traceback):
4142
"""
4243
# In Python 3, all exception info is contained in one object.
4344
raise exc_value
45+
46+
try:
47+
from shutil import which
48+
except ImportError:
49+
# shutil.which() from Python 3.6
50+
# "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
51+
# 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation;
52+
# All Rights Reserved"
53+
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
54+
"""Given a command, mode, and a PATH string, return the path which
55+
conforms to the given mode on the PATH, or None if there is no such
56+
file.
57+
58+
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
59+
of os.environ.get("PATH"), or can be overridden with a custom search
60+
path.
61+
62+
"""
63+
# Check that a given file can be accessed with the correct mode.
64+
# Additionally check that `file` is not a directory, as on Windows
65+
# directories pass the os.access check.
66+
def _access_check(fn, mode):
67+
return (os.path.exists(fn) and os.access(fn, mode)
68+
and not os.path.isdir(fn))
69+
70+
# If we're given a path with a directory part, look it up directly rather
71+
# than referring to PATH directories. This includes checking relative to the
72+
# current directory, e.g. ./script
73+
if os.path.dirname(cmd):
74+
if _access_check(cmd, mode):
75+
return cmd
76+
return None
77+
78+
if path is None:
79+
path = os.environ.get("PATH", os.defpath)
80+
if not path:
81+
return None
82+
path = path.split(os.pathsep)
83+
84+
if sys.platform == "win32":
85+
# The current directory takes precedence on Windows.
86+
if not os.curdir in path:
87+
path.insert(0, os.curdir)
88+
89+
# PATHEXT is necessary to check on Windows.
90+
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
91+
# See if the given file matches any of the expected path extensions.
92+
# This will allow us to short circuit when given "python.exe".
93+
# If it does match, only test that one, otherwise we have to try
94+
# others.
95+
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
96+
files = [cmd]
97+
else:
98+
files = [cmd + ext for ext in pathext]
99+
else:
100+
# On other platforms you don't have things like PATHEXT to tell you
101+
# what file suffixes are executable, so just pass on cmd as-is.
102+
files = [cmd]
103+
104+
seen = set()
105+
for dir in path:
106+
normdir = os.path.normcase(dir)
107+
if not normdir in seen:
108+
seen.add(normdir)
109+
for thefile in files:
110+
name = os.path.join(dir, thefile)
111+
if _access_check(name, mode):
112+
return name
113+
return None

Lib/slapdtest/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
__version__ = '3.0.0b2'
99

1010
from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler
11-
from slapdtest._slapdtest import skip_unless_ci, requires_sasl, requires_tls
11+
from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls
12+
from slapdtest._slapdtest import skip_unless_ci

Lib/slapdtest/_slapdtest.py

Lines changed: 82 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import os
1111
import socket
12+
import sys
1213
import time
1314
import subprocess
1415
import logging
@@ -20,7 +21,7 @@
2021
os.environ['LDAPNOINIT'] = '1'
2122

2223
import ldap
23-
from ldap.compat import quote_plus
24+
from ldap.compat import quote_plus, which
2425

2526
HERE = os.path.abspath(os.path.dirname(__file__))
2627

@@ -56,6 +57,12 @@
5657

5758
LOCALHOST = '127.0.0.1'
5859

60+
CI_DISABLED = set(os.environ.get('CI_DISABLED', '').split(':'))
61+
if 'LDAPI' in CI_DISABLED:
62+
HAVE_LDAPI = False
63+
else:
64+
HAVE_LDAPI = hasattr(socket, 'AF_UNIX')
65+
5966

6067
def identity(test_item):
6168
"""Identity decorator
@@ -69,7 +76,7 @@ def skip_unless_ci(reason, feature=None):
6976
"""
7077
if not os.environ.get('CI', False):
7178
return unittest.skip(reason)
72-
elif feature in os.environ.get('CI_DISABLED', '').split(':'):
79+
elif feature in CI_DISABLED:
7380
return unittest.skip(reason)
7481
else:
7582
# Don't skip on Travis
@@ -95,6 +102,22 @@ def requires_sasl():
95102
return identity
96103

97104

105+
def requires_ldapi():
106+
if not HAVE_LDAPI:
107+
return skip_unless_ci(
108+
"test needs ldapi support (AF_UNIX)", feature='LDAPI')
109+
else:
110+
return identity
111+
112+
def _add_sbin(path):
113+
"""Add /sbin and related directories to a command search path"""
114+
directories = path.split(os.pathsep)
115+
if sys.platform != 'win32':
116+
for sbin in '/usr/local/sbin', '/sbin', '/usr/sbin':
117+
if sbin not in directories:
118+
directories.append(sbin)
119+
return os.pathsep.join(directories)
120+
98121
def combined_logger(
99122
log_name,
100123
log_level=logging.WARN,
@@ -149,8 +172,6 @@ class SlapdObject(object):
149172
root_dn = 'cn=%s,%s' % (root_cn, suffix)
150173
root_pw = 'password'
151174
slapd_loglevel = 'stats stats2'
152-
# use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools
153-
cli_sasl_external = True
154175
local_host = '127.0.0.1'
155176
testrunsubdirs = (
156177
'schema',
@@ -160,8 +181,6 @@ class SlapdObject(object):
160181
)
161182

162183
TMPDIR = os.environ.get('TMP', os.getcwd())
163-
SBINDIR = os.environ.get('SBIN', '/usr/sbin')
164-
BINDIR = os.environ.get('BIN', '/usr/bin')
165184
if 'SCHEMA' in os.environ:
166185
SCHEMADIR = os.environ['SCHEMA']
167186
elif os.path.isdir("/etc/openldap/schema"):
@@ -170,12 +189,9 @@ class SlapdObject(object):
170189
SCHEMADIR = "/etc/ldap/schema"
171190
else:
172191
SCHEMADIR = None
173-
PATH_LDAPADD = os.path.join(BINDIR, 'ldapadd')
174-
PATH_LDAPDELETE = os.path.join(BINDIR, 'ldapdelete')
175-
PATH_LDAPMODIFY = os.path.join(BINDIR, 'ldapmodify')
176-
PATH_LDAPWHOAMI = os.path.join(BINDIR, 'ldapwhoami')
177-
PATH_SLAPD = os.environ.get('SLAPD', os.path.join(SBINDIR, 'slapd'))
178-
PATH_SLAPTEST = os.path.join(SBINDIR, 'slaptest')
192+
193+
BIN_PATH = os.environ.get('BIN', os.environ.get('PATH', os.defpath))
194+
SBIN_PATH = os.environ.get('SBIN', _add_sbin(BIN_PATH))
179195

180196
# time in secs to wait before trying to access slapd via LDAP (again)
181197
_start_sleep = 1.5
@@ -192,25 +208,55 @@ def __init__(self):
192208
self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf')
193209
self._db_directory = os.path.join(self.testrundir, "openldap-data")
194210
self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port)
195-
ldapi_path = os.path.join(self.testrundir, 'ldapi')
196-
self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path)
211+
if HAVE_LDAPI:
212+
ldapi_path = os.path.join(self.testrundir, 'ldapi')
213+
self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path)
214+
self.default_ldap_uri = self.ldapi_uri
215+
# use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools
216+
self.cli_sasl_external = True
217+
else:
218+
self.ldapi_uri = None
219+
self.default_ldap_uri = self.ldap_uri
220+
# Use simple bind via LDAP uri
221+
self.cli_sasl_external = False
222+
223+
self._find_commands()
224+
225+
if self.SCHEMADIR is None:
226+
raise ValueError('SCHEMADIR is None, ldap schemas are missing.')
227+
197228
# TLS certs
198229
self.cafile = os.path.join(HERE, 'certs/ca.pem')
199230
self.servercert = os.path.join(HERE, 'certs/server.pem')
200231
self.serverkey = os.path.join(HERE, 'certs/server.key')
201232
self.clientcert = os.path.join(HERE, 'certs/client.pem')
202233
self.clientkey = os.path.join(HERE, 'certs/client.key')
203234

204-
def _check_requirements(self):
205-
binaries = [
206-
self.PATH_LDAPADD, self.PATH_LDAPMODIFY, self.PATH_LDAPWHOAMI,
207-
self.PATH_SLAPD, self.PATH_SLAPTEST
208-
]
209-
for binary in binaries:
210-
if not os.path.isfile(binary):
211-
raise ValueError('Binary {} is missing.'.format(binary))
212-
if self.SCHEMADIR is None:
213-
raise ValueError('SCHEMADIR is None, ldap schemas are missing.')
235+
def _find_commands(self):
236+
self.PATH_LDAPADD = self._find_command('ldapadd')
237+
self.PATH_LDAPDELETE = self._find_command('ldapdelete')
238+
self.PATH_LDAPMODIFY = self._find_command('ldapmodify')
239+
self.PATH_LDAPWHOAMI = self._find_command('ldapwhoami')
240+
241+
self.PATH_SLAPD = os.environ.get('SLAPD', None)
242+
if not self.PATH_SLAPD:
243+
self.PATH_SLAPD = self._find_command('slapd', in_sbin=True)
244+
self.PATH_SLAPTEST = self._find_command('slaptest', in_sbin=True)
245+
246+
def _find_command(self, cmd, in_sbin=False):
247+
if in_sbin:
248+
path = self.SBIN_PATH
249+
var_name = 'SBIN'
250+
else:
251+
path = self.BIN_PATH
252+
var_name = 'BIN'
253+
command = which(cmd, path=path)
254+
if command is None:
255+
raise ValueError(
256+
"Command '{}' not found. Set the {} environment variable to "
257+
"override slapdtest's search path.".format(value, var_name)
258+
)
259+
return command
214260

215261
def setup_rundir(self):
216262
"""
@@ -331,11 +377,14 @@ def _start_slapd(self):
331377
"""
332378
Spawns/forks the slapd process
333379
"""
380+
urls = [self.ldap_uri]
381+
if self.ldapi_uri:
382+
urls.append(self.ldapi_uri)
334383
slapd_args = [
335384
self.PATH_SLAPD,
336385
'-f', self._slapd_conf,
337386
'-F', self.testrundir,
338-
'-h', '%s' % ' '.join((self.ldap_uri, self.ldapi_uri)),
387+
'-h', ' '.join(urls),
339388
]
340389
if self._log.isEnabledFor(logging.DEBUG):
341390
slapd_args.extend(['-d', '-1'])
@@ -346,26 +395,28 @@ def _start_slapd(self):
346395
# Waits until the LDAP server socket is open, or slapd crashed
347396
# no cover to avoid spurious coverage changes, see
348397
# https://github.com/python-ldap/python-ldap/issues/127
349-
while 1: # pragma: no cover
398+
for _ in range(10): # pragma: no cover
350399
if self._proc.poll() is not None:
351400
self._stopped()
352401
raise RuntimeError("slapd exited before opening port")
353402
time.sleep(self._start_sleep)
354403
try:
355-
self._log.debug("slapd connection check to %s", self.ldapi_uri)
404+
self._log.debug(
405+
"slapd connection check to %s", self.default_ldap_uri
406+
)
356407
self.ldapwhoami()
357408
except RuntimeError:
358409
pass
359410
else:
360411
return
412+
raise RuntimeError("slapd did not start properly")
361413

362414
def start(self):
363415
"""
364416
Starts the slapd server process running, and waits for it to come up.
365417
"""
366418

367419
if self._proc is None:
368-
self._check_requirements()
369420
# prepare directory structure
370421
atexit.register(self.stop)
371422
self._cleanup_rundir()
@@ -435,9 +486,11 @@ def _cli_auth_args(self):
435486
# no cover to avoid spurious coverage changes
436487
def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None,
437488
stdin_data=None): # pragma: no cover
489+
if ldap_uri is None:
490+
ldap_uri = self.default_ldap_uri
438491
args = [
439492
ldapcommand,
440-
'-H', ldap_uri or self.ldapi_uri,
493+
'-H', ldap_uri,
441494
] + self._cli_auth_args() + (extra_args or [])
442495
self._log.debug('Run command: %r', ' '.join(args))
443496
proc = subprocess.Popen(

Tests/t_ldap_sasl.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
See https://www.python-ldap.org/ for details.
66
"""
77
import os
8-
import pwd
98
import socket
109
import unittest
1110

@@ -14,7 +13,8 @@
1413

1514
from ldap.ldapobject import SimpleLDAPObject
1615
import ldap.sasl
17-
from slapdtest import SlapdTestCase, requires_sasl, requires_tls
16+
from slapdtest import SlapdTestCase
17+
from slapdtest import requires_ldapi, requires_sasl, requires_tls
1818

1919

2020
LDIF = """
@@ -60,7 +60,7 @@ def setUpClass(cls):
6060
)
6161
cls.server.ldapadd(ldif)
6262

63-
@unittest.skipUnless(hasattr(socket, 'AF_UNIX'), "needs Unix socket")
63+
@requires_ldapi()
6464
def test_external_ldapi(self):
6565
# EXTERNAL authentication with LDAPI (AF_UNIX)
6666
ldap_conn = self.ldap_object_class(self.server.ldapi_uri)

Tests/t_ldap_schema_subentry.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ldap.ldapobject import SimpleLDAPObject
1717
import ldap.schema
1818
from ldap.schema.models import ObjectClass
19-
from slapdtest import SlapdTestCase
19+
from slapdtest import SlapdTestCase, requires_ldapi
2020

2121
HERE = os.path.abspath(os.path.dirname(__file__))
2222

@@ -88,6 +88,7 @@ def test_urlfetch_ldap(self):
8888
dn, schema = ldap.schema.urlfetch(self.server.ldap_uri)
8989
self.assertSlapdSchema(dn, schema)
9090

91+
@requires_ldapi()
9192
def test_urlfetch_ldapi(self):
9293
dn, schema = ldap.schema.urlfetch(self.server.ldapi_uri)
9394
self.assertSlapdSchema(dn, schema)

0 commit comments

Comments
 (0)