1# -*- coding: utf-8 -*-
2"""sdist tests"""
3
4import contextlib
5import os
6import shutil
7import sys
8import tempfile
9import itertools
10import io
11from distutils import log
12from distutils.errors import DistutilsTemplateError
13
14from setuptools.command.egg_info import FileList, egg_info, translate_pattern
15from setuptools.dist import Distribution
16from setuptools.tests.textwrap import DALS
17
18import pytest
19
20
21def make_local_path(s):
22    """Converts '/' in a string to os.sep"""
23    return s.replace('/', os.sep)
24
25
26SETUP_ATTRS = {
27    'name': 'app',
28    'version': '0.0',
29    'packages': ['app'],
30}
31
32SETUP_PY = """\
33from setuptools import setup
34
35setup(**%r)
36""" % SETUP_ATTRS
37
38
39@contextlib.contextmanager
40def quiet():
41    old_stdout, old_stderr = sys.stdout, sys.stderr
42    sys.stdout, sys.stderr = io.StringIO(), io.StringIO()
43    try:
44        yield
45    finally:
46        sys.stdout, sys.stderr = old_stdout, old_stderr
47
48
49def touch(filename):
50    open(filename, 'w').close()
51
52
53# The set of files always in the manifest, including all files in the
54# .egg-info directory
55default_files = frozenset(map(make_local_path, [
56    'README.rst',
57    'MANIFEST.in',
58    'setup.py',
59    'app.egg-info/PKG-INFO',
60    'app.egg-info/SOURCES.txt',
61    'app.egg-info/dependency_links.txt',
62    'app.egg-info/top_level.txt',
63    'app/__init__.py',
64]))
65
66
67translate_specs = [
68    ('foo', ['foo'], ['bar', 'foobar']),
69    ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']),
70
71    # Glob matching
72    ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']),
73    (
74        'dir/*.txt',
75        ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']),
76    ('*/*.py', ['bin/start.py'], []),
77    ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']),
78
79    # Globstars change what they mean depending upon where they are
80    (
81        'foo/**/bar',
82        ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'],
83        ['foo/abar'],
84    ),
85    (
86        'foo/**',
87        ['foo/bar/bing.py', 'foo/x'],
88        ['/foo/x'],
89    ),
90    (
91        '**',
92        ['x', 'abc/xyz', '@nything'],
93        [],
94    ),
95
96    # Character classes
97    (
98        'pre[one]post',
99        ['preopost', 'prenpost', 'preepost'],
100        ['prepost', 'preonepost'],
101    ),
102
103    (
104        'hello[!one]world',
105        ['helloxworld', 'helloyworld'],
106        ['hellooworld', 'helloworld', 'hellooneworld'],
107    ),
108
109    (
110        '[]one].txt',
111        ['o.txt', '].txt', 'e.txt'],
112        ['one].txt'],
113    ),
114
115    (
116        'foo[!]one]bar',
117        ['fooybar'],
118        ['foo]bar', 'fooobar', 'fooebar'],
119    ),
120
121]
122"""
123A spec of inputs for 'translate_pattern' and matches and mismatches
124for that input.
125"""
126
127match_params = itertools.chain.from_iterable(
128    zip(itertools.repeat(pattern), matches)
129    for pattern, matches, mismatches in translate_specs
130)
131
132
133@pytest.fixture(params=match_params)
134def pattern_match(request):
135    return map(make_local_path, request.param)
136
137
138mismatch_params = itertools.chain.from_iterable(
139    zip(itertools.repeat(pattern), mismatches)
140    for pattern, matches, mismatches in translate_specs
141)
142
143
144@pytest.fixture(params=mismatch_params)
145def pattern_mismatch(request):
146    return map(make_local_path, request.param)
147
148
149def test_translated_pattern_match(pattern_match):
150    pattern, target = pattern_match
151    assert translate_pattern(pattern).match(target)
152
153
154def test_translated_pattern_mismatch(pattern_mismatch):
155    pattern, target = pattern_mismatch
156    assert not translate_pattern(pattern).match(target)
157
158
159class TempDirTestCase:
160    def setup_method(self, method):
161        self.temp_dir = tempfile.mkdtemp()
162        self.old_cwd = os.getcwd()
163        os.chdir(self.temp_dir)
164
165    def teardown_method(self, method):
166        os.chdir(self.old_cwd)
167        shutil.rmtree(self.temp_dir)
168
169
170class TestManifestTest(TempDirTestCase):
171    def setup_method(self, method):
172        super(TestManifestTest, self).setup_method(method)
173
174        f = open(os.path.join(self.temp_dir, 'setup.py'), 'w')
175        f.write(SETUP_PY)
176        f.close()
177        """
178        Create a file tree like:
179        - LICENSE
180        - README.rst
181        - testing.rst
182        - .hidden.rst
183        - app/
184            - __init__.py
185            - a.txt
186            - b.txt
187            - c.rst
188            - static/
189                - app.js
190                - app.js.map
191                - app.css
192                - app.css.map
193        """
194
195        for fname in ['README.rst', '.hidden.rst', 'testing.rst', 'LICENSE']:
196            touch(os.path.join(self.temp_dir, fname))
197
198        # Set up the rest of the test package
199        test_pkg = os.path.join(self.temp_dir, 'app')
200        os.mkdir(test_pkg)
201        for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']:
202            touch(os.path.join(test_pkg, fname))
203
204        # Some compiled front-end assets to include
205        static = os.path.join(test_pkg, 'static')
206        os.mkdir(static)
207        for fname in ['app.js', 'app.js.map', 'app.css', 'app.css.map']:
208            touch(os.path.join(static, fname))
209
210    def make_manifest(self, contents):
211        """Write a MANIFEST.in."""
212        with open(os.path.join(self.temp_dir, 'MANIFEST.in'), 'w') as f:
213            f.write(DALS(contents))
214
215    def get_files(self):
216        """Run egg_info and get all the files to include, as a set"""
217        dist = Distribution(SETUP_ATTRS)
218        dist.script_name = 'setup.py'
219        cmd = egg_info(dist)
220        cmd.ensure_finalized()
221
222        cmd.run()
223
224        return set(cmd.filelist.files)
225
226    def test_no_manifest(self):
227        """Check a missing MANIFEST.in includes only the standard files."""
228        assert (default_files - set(['MANIFEST.in'])) == self.get_files()
229
230    def test_empty_files(self):
231        """Check an empty MANIFEST.in includes only the standard files."""
232        self.make_manifest("")
233        assert default_files == self.get_files()
234
235    def test_include(self):
236        """Include extra rst files in the project root."""
237        self.make_manifest("include *.rst")
238        files = default_files | set([
239            'testing.rst', '.hidden.rst'])
240        assert files == self.get_files()
241
242    def test_exclude(self):
243        """Include everything in app/ except the text files"""
244        ml = make_local_path
245        self.make_manifest(
246            """
247            include app/*
248            exclude app/*.txt
249            """)
250        files = default_files | set([ml('app/c.rst')])
251        assert files == self.get_files()
252
253    def test_include_multiple(self):
254        """Include with multiple patterns."""
255        ml = make_local_path
256        self.make_manifest("include app/*.txt app/static/*")
257        files = default_files | set([
258            ml('app/a.txt'), ml('app/b.txt'),
259            ml('app/static/app.js'), ml('app/static/app.js.map'),
260            ml('app/static/app.css'), ml('app/static/app.css.map')])
261        assert files == self.get_files()
262
263    def test_graft(self):
264        """Include the whole app/static/ directory."""
265        ml = make_local_path
266        self.make_manifest("graft app/static")
267        files = default_files | set([
268            ml('app/static/app.js'), ml('app/static/app.js.map'),
269            ml('app/static/app.css'), ml('app/static/app.css.map')])
270        assert files == self.get_files()
271
272    def test_graft_glob_syntax(self):
273        """Include the whole app/static/ directory."""
274        ml = make_local_path
275        self.make_manifest("graft */static")
276        files = default_files | set([
277            ml('app/static/app.js'), ml('app/static/app.js.map'),
278            ml('app/static/app.css'), ml('app/static/app.css.map')])
279        assert files == self.get_files()
280
281    def test_graft_global_exclude(self):
282        """Exclude all *.map files in the project."""
283        ml = make_local_path
284        self.make_manifest(
285            """
286            graft app/static
287            global-exclude *.map
288            """)
289        files = default_files | set([
290            ml('app/static/app.js'), ml('app/static/app.css')])
291        assert files == self.get_files()
292
293    def test_global_include(self):
294        """Include all *.rst, *.js, and *.css files in the whole tree."""
295        ml = make_local_path
296        self.make_manifest(
297            """
298            global-include *.rst *.js *.css
299            """)
300        files = default_files | set([
301            '.hidden.rst', 'testing.rst', ml('app/c.rst'),
302            ml('app/static/app.js'), ml('app/static/app.css')])
303        assert files == self.get_files()
304
305    def test_graft_prune(self):
306        """Include all files in app/, except for the whole app/static/ dir."""
307        ml = make_local_path
308        self.make_manifest(
309            """
310            graft app
311            prune app/static
312            """)
313        files = default_files | set([
314            ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')])
315        assert files == self.get_files()
316
317
318class TestFileListTest(TempDirTestCase):
319    """
320    A copy of the relevant bits of distutils/tests/test_filelist.py,
321    to ensure setuptools' version of FileList keeps parity with distutils.
322    """
323
324    def setup_method(self, method):
325        super(TestFileListTest, self).setup_method(method)
326        self.threshold = log.set_threshold(log.FATAL)
327        self._old_log = log.Log._log
328        log.Log._log = self._log
329        self.logs = []
330
331    def teardown_method(self, method):
332        log.set_threshold(self.threshold)
333        log.Log._log = self._old_log
334        super(TestFileListTest, self).teardown_method(method)
335
336    def _log(self, level, msg, args):
337        if level not in (log.DEBUG, log.INFO, log.WARN, log.ERROR, log.FATAL):
338            raise ValueError('%s wrong log level' % str(level))
339        self.logs.append((level, msg, args))
340
341    def get_logs(self, *levels):
342        def _format(msg, args):
343            if len(args) == 0:
344                return msg
345            return msg % args
346        return [_format(msg, args) for level, msg, args
347                in self.logs if level in levels]
348
349    def clear_logs(self):
350        self.logs = []
351
352    def assertNoWarnings(self):
353        assert self.get_logs(log.WARN) == []
354        self.clear_logs()
355
356    def assertWarnings(self):
357        assert len(self.get_logs(log.WARN)) > 0
358        self.clear_logs()
359
360    def make_files(self, files):
361        for file in files:
362            file = os.path.join(self.temp_dir, file)
363            dirname, basename = os.path.split(file)
364            os.makedirs(dirname, exist_ok=True)
365            open(file, 'w').close()
366
367    def test_process_template_line(self):
368        # testing  all MANIFEST.in template patterns
369        file_list = FileList()
370        ml = make_local_path
371
372        # simulated file list
373        self.make_files([
374            'foo.tmp', 'ok', 'xo', 'four.txt',
375            'buildout.cfg',
376            # filelist does not filter out VCS directories,
377            # it's sdist that does
378            ml('.hg/last-message.txt'),
379            ml('global/one.txt'),
380            ml('global/two.txt'),
381            ml('global/files.x'),
382            ml('global/here.tmp'),
383            ml('f/o/f.oo'),
384            ml('dir/graft-one'),
385            ml('dir/dir2/graft2'),
386            ml('dir3/ok'),
387            ml('dir3/sub/ok.txt'),
388        ])
389
390        MANIFEST_IN = DALS("""\
391        include ok
392        include xo
393        exclude xo
394        include foo.tmp
395        include buildout.cfg
396        global-include *.x
397        global-include *.txt
398        global-exclude *.tmp
399        recursive-include f *.oo
400        recursive-exclude global *.x
401        graft dir
402        prune dir3
403        """)
404
405        for line in MANIFEST_IN.split('\n'):
406            if not line:
407                continue
408            file_list.process_template_line(line)
409
410        wanted = [
411            'buildout.cfg',
412            'four.txt',
413            'ok',
414            ml('.hg/last-message.txt'),
415            ml('dir/graft-one'),
416            ml('dir/dir2/graft2'),
417            ml('f/o/f.oo'),
418            ml('global/one.txt'),
419            ml('global/two.txt'),
420        ]
421
422        file_list.sort()
423        assert file_list.files == wanted
424
425    def test_exclude_pattern(self):
426        # return False if no match
427        file_list = FileList()
428        assert not file_list.exclude_pattern('*.py')
429
430        # return True if files match
431        file_list = FileList()
432        file_list.files = ['a.py', 'b.py']
433        assert file_list.exclude_pattern('*.py')
434
435        # test excludes
436        file_list = FileList()
437        file_list.files = ['a.py', 'a.txt']
438        file_list.exclude_pattern('*.py')
439        file_list.sort()
440        assert file_list.files == ['a.txt']
441
442    def test_include_pattern(self):
443        # return False if no match
444        file_list = FileList()
445        self.make_files([])
446        assert not file_list.include_pattern('*.py')
447
448        # return True if files match
449        file_list = FileList()
450        self.make_files(['a.py', 'b.txt'])
451        assert file_list.include_pattern('*.py')
452
453        # test * matches all files
454        file_list = FileList()
455        self.make_files(['a.py', 'b.txt'])
456        file_list.include_pattern('*')
457        file_list.sort()
458        assert file_list.files == ['a.py', 'b.txt']
459
460    def test_process_template_line_invalid(self):
461        # invalid lines
462        file_list = FileList()
463        for action in ('include', 'exclude', 'global-include',
464                       'global-exclude', 'recursive-include',
465                       'recursive-exclude', 'graft', 'prune', 'blarg'):
466            try:
467                file_list.process_template_line(action)
468            except DistutilsTemplateError:
469                pass
470            except Exception:
471                assert False, "Incorrect error thrown"
472            else:
473                assert False, "Should have thrown an error"
474
475    def test_include(self):
476        ml = make_local_path
477        # include
478        file_list = FileList()
479        self.make_files(['a.py', 'b.txt', ml('d/c.py')])
480
481        file_list.process_template_line('include *.py')
482        file_list.sort()
483        assert file_list.files == ['a.py']
484        self.assertNoWarnings()
485
486        file_list.process_template_line('include *.rb')
487        file_list.sort()
488        assert file_list.files == ['a.py']
489        self.assertWarnings()
490
491    def test_exclude(self):
492        ml = make_local_path
493        # exclude
494        file_list = FileList()
495        file_list.files = ['a.py', 'b.txt', ml('d/c.py')]
496
497        file_list.process_template_line('exclude *.py')
498        file_list.sort()
499        assert file_list.files == ['b.txt', ml('d/c.py')]
500        self.assertNoWarnings()
501
502        file_list.process_template_line('exclude *.rb')
503        file_list.sort()
504        assert file_list.files == ['b.txt', ml('d/c.py')]
505        self.assertWarnings()
506
507    def test_global_include(self):
508        ml = make_local_path
509        # global-include
510        file_list = FileList()
511        self.make_files(['a.py', 'b.txt', ml('d/c.py')])
512
513        file_list.process_template_line('global-include *.py')
514        file_list.sort()
515        assert file_list.files == ['a.py', ml('d/c.py')]
516        self.assertNoWarnings()
517
518        file_list.process_template_line('global-include *.rb')
519        file_list.sort()
520        assert file_list.files == ['a.py', ml('d/c.py')]
521        self.assertWarnings()
522
523    def test_global_exclude(self):
524        ml = make_local_path
525        # global-exclude
526        file_list = FileList()
527        file_list.files = ['a.py', 'b.txt', ml('d/c.py')]
528
529        file_list.process_template_line('global-exclude *.py')
530        file_list.sort()
531        assert file_list.files == ['b.txt']
532        self.assertNoWarnings()
533
534        file_list.process_template_line('global-exclude *.rb')
535        file_list.sort()
536        assert file_list.files == ['b.txt']
537        self.assertWarnings()
538
539    def test_recursive_include(self):
540        ml = make_local_path
541        # recursive-include
542        file_list = FileList()
543        self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')])
544
545        file_list.process_template_line('recursive-include d *.py')
546        file_list.sort()
547        assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
548        self.assertNoWarnings()
549
550        file_list.process_template_line('recursive-include e *.py')
551        file_list.sort()
552        assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
553        self.assertWarnings()
554
555    def test_recursive_exclude(self):
556        ml = make_local_path
557        # recursive-exclude
558        file_list = FileList()
559        file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]
560
561        file_list.process_template_line('recursive-exclude d *.py')
562        file_list.sort()
563        assert file_list.files == ['a.py', ml('d/c.txt')]
564        self.assertNoWarnings()
565
566        file_list.process_template_line('recursive-exclude e *.py')
567        file_list.sort()
568        assert file_list.files == ['a.py', ml('d/c.txt')]
569        self.assertWarnings()
570
571    def test_graft(self):
572        ml = make_local_path
573        # graft
574        file_list = FileList()
575        self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')])
576
577        file_list.process_template_line('graft d')
578        file_list.sort()
579        assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
580        self.assertNoWarnings()
581
582        file_list.process_template_line('graft e')
583        file_list.sort()
584        assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
585        self.assertWarnings()
586
587    def test_prune(self):
588        ml = make_local_path
589        # prune
590        file_list = FileList()
591        file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]
592
593        file_list.process_template_line('prune d')
594        file_list.sort()
595        assert file_list.files == ['a.py', ml('f/f.py')]
596        self.assertNoWarnings()
597
598        file_list.process_template_line('prune e')
599        file_list.sort()
600        assert file_list.files == ['a.py', ml('f/f.py')]
601        self.assertWarnings()
602