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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy