1import py
2import sys, os, re
3import shutil, subprocess, time
4from testing.udir import udir
5import cffi
6
7
8local_dir = os.path.dirname(os.path.abspath(__file__))
9_link_error = '?'
10
11def check_lib_python_found(tmpdir):
12    global _link_error
13    if _link_error == '?':
14        ffi = cffi.FFI()
15        kwds = {}
16        ffi._apply_embedding_fix(kwds)
17        ffi.set_source("_test_lib_python_found", "", **kwds)
18        try:
19            ffi.compile(tmpdir=tmpdir, verbose=True)
20        except cffi.VerificationError as e:
21            _link_error = e
22        else:
23            _link_error = None
24    if _link_error:
25        py.test.skip(str(_link_error))
26
27
28def prefix_pythonpath():
29    cffi_base = os.path.dirname(os.path.dirname(local_dir))
30    pythonpath = org_env.get('PYTHONPATH', '').split(os.pathsep)
31    if cffi_base not in pythonpath:
32        pythonpath.insert(0, cffi_base)
33    return os.pathsep.join(pythonpath)
34
35def copy_away_env():
36    global org_env
37    try:
38        org_env
39    except NameError:
40        org_env = os.environ.copy()
41
42
43class EmbeddingTests:
44    _compiled_modules = {}
45
46    def setup_method(self, meth):
47        check_lib_python_found(str(udir.ensure('embedding', dir=1)))
48        self._path = udir.join('embedding', meth.__name__)
49        if sys.platform == "win32" or sys.platform == "darwin":
50            self._compiled_modules.clear()   # workaround
51
52    def get_path(self):
53        return str(self._path.ensure(dir=1))
54
55    def _run_base(self, args, **kwds):
56        print('RUNNING:', args, kwds)
57        return subprocess.Popen(args, **kwds)
58
59    def _run(self, args):
60        popen = self._run_base(args, cwd=self.get_path(),
61                                 stdout=subprocess.PIPE,
62                                 universal_newlines=True)
63        output = popen.stdout.read()
64        err = popen.wait()
65        if err:
66            raise OSError("popen failed with exit code %r: %r" % (
67                err, args))
68        print(output.rstrip())
69        return output
70
71    def prepare_module(self, name):
72        self.patch_environment()
73        if name not in self._compiled_modules:
74            path = self.get_path()
75            filename = '%s.py' % name
76            # NOTE: if you have an .egg globally installed with an older
77            # version of cffi, this will not work, because sys.path ends
78            # up with the .egg before the PYTHONPATH entries.  I didn't
79            # find a solution to that: we could hack sys.path inside the
80            # script run here, but we can't hack it in the same way in
81            # execute().
82            pathname = os.path.join(path, filename)
83            with open(pathname, 'w') as g:
84                g.write('''
85# https://bugs.python.org/issue23246
86import sys
87if sys.platform == 'win32':
88    try:
89        import setuptools
90    except ImportError:
91        pass
92''')
93                with open(os.path.join(local_dir, filename), 'r') as f:
94                    g.write(f.read())
95
96            output = self._run([sys.executable, pathname])
97            match = re.compile(r"\bFILENAME: (.+)").search(output)
98            assert match
99            dynamic_lib_name = match.group(1)
100            if sys.platform == 'win32':
101                assert dynamic_lib_name.endswith('_cffi.dll')
102            elif sys.platform == 'darwin':
103                assert dynamic_lib_name.endswith('_cffi.dylib')
104            else:
105                assert dynamic_lib_name.endswith('_cffi.so')
106            self._compiled_modules[name] = dynamic_lib_name
107        return self._compiled_modules[name]
108
109    def compile(self, name, modules, opt=False, threads=False, defines={}):
110        path = self.get_path()
111        filename = '%s.c' % name
112        shutil.copy(os.path.join(local_dir, filename), path)
113        shutil.copy(os.path.join(local_dir, 'thread-test.h'), path)
114        import distutils.ccompiler
115        curdir = os.getcwd()
116        try:
117            os.chdir(self.get_path())
118            c = distutils.ccompiler.new_compiler()
119            print('compiling %s with %r' % (name, modules))
120            extra_preargs = []
121            debug = True
122            if sys.platform == 'win32':
123                libfiles = []
124                for m in modules:
125                    m = os.path.basename(m)
126                    assert m.endswith('.dll')
127                    libfiles.append('Release\\%s.lib' % m[:-4])
128                modules = libfiles
129                extra_preargs.append('/MANIFEST')
130                debug = False    # you need to install extra stuff
131                                 # for this to work
132            elif threads:
133                extra_preargs.append('-pthread')
134            objects = c.compile([filename], macros=sorted(defines.items()),
135                                debug=debug)
136            c.link_executable(objects + modules, name, extra_preargs=extra_preargs)
137        finally:
138            os.chdir(curdir)
139
140    def patch_environment(self):
141        copy_away_env()
142        path = self.get_path()
143        # for libpypy-c.dll or Python27.dll
144        path = os.path.split(sys.executable)[0] + os.path.pathsep + path
145        env_extra = {'PYTHONPATH': prefix_pythonpath()}
146        if sys.platform == 'win32':
147            envname = 'PATH'
148        else:
149            envname = 'LD_LIBRARY_PATH'
150        libpath = org_env.get(envname)
151        if libpath:
152            libpath = path + os.path.pathsep + libpath
153        else:
154            libpath = path
155        env_extra[envname] = libpath
156        for key, value in sorted(env_extra.items()):
157            if os.environ.get(key) != value:
158                print('* setting env var %r to %r' % (key, value))
159                os.environ[key] = value
160
161    def execute(self, name):
162        path = self.get_path()
163        print('running %r in %r' % (name, path))
164        executable_name = name
165        if sys.platform == 'win32':
166            executable_name = os.path.join(path, executable_name + '.exe')
167        else:
168            executable_name = os.path.join('.', executable_name)
169        popen = self._run_base([executable_name], cwd=path,
170                               stdout=subprocess.PIPE,
171                               universal_newlines=True)
172        result = popen.stdout.read()
173        err = popen.wait()
174        if err:
175            raise OSError("%r failed with exit code %r" % (name, err))
176        return result
177
178
179class TestBasic(EmbeddingTests):
180    def test_empty(self):
181        empty_cffi = self.prepare_module('empty')
182
183    def test_basic(self):
184        add1_cffi = self.prepare_module('add1')
185        self.compile('add1-test', [add1_cffi])
186        output = self.execute('add1-test')
187        assert output == ("preparing...\n"
188                          "adding 40 and 2\n"
189                          "adding 100 and -5\n"
190                          "got: 42 95\n")
191
192    def test_two_modules(self):
193        add1_cffi = self.prepare_module('add1')
194        add2_cffi = self.prepare_module('add2')
195        self.compile('add2-test', [add1_cffi, add2_cffi])
196        output = self.execute('add2-test')
197        assert output == ("preparing...\n"
198                          "adding 40 and 2\n"
199                          "prepADD2\n"
200                          "adding 100 and -5 and -20\n"
201                          "got: 42 75\n")
202
203    def test_init_time_error(self):
204        initerror_cffi = self.prepare_module('initerror')
205        self.compile('add1-test', [initerror_cffi])
206        output = self.execute('add1-test')
207        assert output == "got: 0 0\n"    # plus lots of info to stderr
208