1#!/usr/bin/python
2
3# test DistUtilsExtra.auto
4
5import unittest
6import shutil
7import tempfile
8import os
9import subprocess
10
11class T(unittest.TestCase):
12    def setUp(self):
13        self.maxDiff = None
14        self.src = tempfile.mkdtemp()
15
16        self._mksrc('setup.py', '''
17# ignore warning about import from local path
18import warnings
19warnings.filterwarnings('ignore', 'Module DistUtilsExtra was already imported from.*')
20warnings.filterwarnings('ignore', 'pipe2 set errno ENOSYS.*')
21
22from DistUtilsExtra.auto import setup
23
24setup(
25    name='foo',
26    version='0.1',
27    description='Test suite package',
28    url='https://foo.example.com',
29    license='GPL v2 or later',
30    author='Martin Pitt',
31    author_email='martin.pitt@example.com',
32)
33''')
34        self.snapshot = None
35        self.install_tree = None
36
37    def tearDown(self):
38        try:
39            # check that setup.py clean removes everything
40            (o, e, s) = self.setup_py(['clean', '-a'])
41            self.assertEqual(s, 0, o+e)
42            cruft = self.diff_snapshot()
43            self.assertEqual(cruft, '', 'no cruft after cleaning:\n' + cruft)
44        finally:
45            shutil.rmtree(self.src)
46            if self.snapshot:
47                shutil.rmtree(self.snapshot)
48            if self.install_tree:
49                shutil.rmtree(self.install_tree)
50            self.src = None
51            self.snapshot = None
52            self.install_tree = None
53
54    #
55    # actual tests come here
56    #
57
58    def test_empty(self):
59        '''empty source tree (just setup.py)'''
60
61        (o, e, s) = self.do_install()
62        self.assertEqual(e, '')
63        self.assertEqual(s, 0)
64        self.assertNotIn('following files are not recognized', o)
65
66        f = self.installed_files()
67        # just installs the .egg_info
68        self.assertEqual(len(f), 1)
69        self.assertTrue(f[0].endswith('.egg-info'))
70
71    def test_vcs(self):
72        '''Ignores revision control files'''
73
74        self._mksrc('.shelf/1')
75        self._mksrc('.bzr/revs')
76        self._mksrc('.git/config')
77        self._mksrc('.svn/revs')
78
79        (o, e, s) = self.do_install()
80        self.assertEqual(e, '')
81        self.assertEqual(s, 0)
82        self.assertNotIn('following files are not recognized', o)
83
84        f = self.installed_files()
85        # just installs the .egg_info
86        self.assertEqual(len(f), 1)
87        self.assertTrue(f[0].endswith('.egg-info'))
88
89    def test_modules(self):
90        '''Python modules'''
91
92        self._mksrc('yesme.py', b'x ="a\xc3\xa4b\xe2\x99\xa5"'.decode('UTF-8'))
93        self._mksrc('stuff/notme.py', b'x ="a\xc3\xa4b\xe2\x99\xa5"'.decode('UTF-8'))
94        self._mksrc('stuff/withencoding.py', b'# -*- Mode: Python; coding: utf-8; -*- \nfoo = 1'.decode('UTF-8'))
95
96        (o, e, s) = self.do_install()
97        self.assertEqual(e, '')
98        self.assertEqual(s, 0)
99        self.assertIn('following files are not recognized', o)
100        self.assertIn('\n  stuff/notme.py\n', o)
101
102        f = '\n'.join(self.installed_files())
103        self.assertIn('-packages/yesme.py', f)
104        self.assertNotIn('notme', f)
105
106    def test_packages(self):
107        '''Python packages'''
108
109        self._mksrc('foopkg/__init__.py', '')
110        self._mksrc('foopkg/bar.py')
111        self._mksrc('foopkg/baz.py')
112        self._mksrc('noinit/notme.py')
113
114        (o, e, s) = self.do_install()
115        self.assertEqual(e, '')
116        self.assertEqual(s, 0)
117        self.assertIn('following files are not recognized', o)
118        self.assertIn('\n  noinit/notme.py\n', o)
119
120        f = '\n'.join(self.installed_files())
121        self.assertIn('foopkg/__init__.py', f)
122        self.assertIn('foopkg/bar.py', f)
123        self.assertNotIn('noinit', f)
124
125    def test_dbus(self):
126        '''D-BUS configuration and service files'''
127
128        # D-BUS ACL configuration file
129        self._mksrc('daemon/com.example.foo.conf', '''<!DOCTYPE busconfig PUBLIC
130 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
131 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
132<busconfig>
133</busconfig>''')
134
135        # non-D-BUS configuration file
136        self._mksrc('daemon/defaults.conf', 'start = True\nlog = syslog')
137
138        # D-BUS system service
139        self._mksrc('daemon/com.example.foo.service', '''[D-BUS Service]
140Name=com.example.Foo
141Exec=/usr/lib/foo/foo_daemon
142User=root''')
143
144        # D-BUS session service
145        self._mksrc('gui/com.example.foo.gui.service', '''[D-BUS Service]
146Name=com.example.Foo.GUI
147Exec=/usr/bin/foo-gtk
148''')
149
150        # non-D-BUS .service file
151        self._mksrc('stuff/super.service', 'I am a file')
152
153        (o, e, s) = self.do_install()
154        self.assertEqual(e, '')
155        self.assertEqual(s, 0)
156        self.assertIn('following files are not recognized', o)
157        self.assertIn('\n  stuff/super.service\n', o)
158
159        f = self.installed_files()
160        self.assertEqual(len(f), 4) # 3 D-BUS files plus .egg-info
161        self.assertIn('/etc/dbus-1/system.d/com.example.foo.conf', f)
162        self.assertIn('/usr/share/dbus-1/system-services/com.example.foo.service', f)
163        self.assertIn('/usr/share/dbus-1/services/com.example.foo.gui.service', f)
164        self.assertNotIn('super.service', '\n'.join(f))
165
166    def test_gsettings(self):
167        '''GSettings schema files'''
168
169        # schema files in dedicated directory
170        self._mksrc('data/glib-2.0/schemas/org.test.myapp.gschema.xml')
171        self._mksrc('data/glib-2.0/schemas/gschemas.compiled')
172        # schema files in data directory
173        self._mksrc('data/org.test.myapp2.gschema.xml')
174        self._mksrc('data/gschemas.compiled')
175
176        (o, e, s) = self.do_install()
177        self.assertEqual(e, '')
178        self.assertEqual(s, 0)
179
180        f = self.installed_files()
181        self.assertEqual(len(f), 3) # 2 schema files plus .egg-info
182        self.assertIn('/usr/share/glib-2.0/schemas/org.test.myapp.gschema.xml', f)
183        self.assertNotIn('gschemas.compiled', '\n'.join(f))
184
185    def test_apport_hook(self):
186        '''Apport hooks'''
187
188        self._mksrc('apport/foo.py', '''import os
189def add_info(report):
190    pass
191''')
192
193        self._mksrc('apport/source_foo.py', '''import os
194def add_info(report):
195    pass
196''')
197
198        (o, e, s) = self.do_install()
199        self.assertEqual(e, '')
200        self.assertEqual(s, 0)
201        self.assertNotIn('following files are not recognized', o)
202
203        f = self.installed_files()
204        self.assertEqual(len(f), 3, f) # 2 hook files plus .egg-info
205        self.assertIn('/usr/share/apport/package-hooks/foo.py', f)
206        self.assertIn('/usr/share/apport/package-hooks/source_foo.py', f)
207
208    def test_po(self):
209        '''gettext *.po files'''
210
211        self._mkpo()
212
213        (o, e, s) = self.do_install()
214        self.assertEqual(e, '')
215        self.assertEqual(s, 0)
216        self.assertNotIn('following files are not recognized', o)
217        f = self.installed_files()
218        self.assertIn('/usr/share/locale/de/LC_MESSAGES/foo.mo', f)
219        self.assertIn('/usr/share/locale/fr/LC_MESSAGES/foo.mo', f)
220        self.assertNotIn('junk', '\n'.join(f))
221
222        msgunfmt = subprocess.Popen(['msgunfmt',
223            os.path.join(self.install_tree,
224            'usr/share/locale/de/LC_MESSAGES/foo.mo')],
225            stdout=subprocess.PIPE)
226        out = msgunfmt.communicate()[0].decode()
227        self.assertEqual(out, self._src_contents('po/de.po'))
228
229    def test_policykit(self):
230        '''*.policy.in PolicyKit files'''
231
232        self._mksrc('daemon/com.example.foo.policy.in', '''<?xml version="1.0" encoding="UTF-8"?>
233<!DOCTYPE policyconfig PUBLIC
234 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
235 "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
236<policyconfig>
237  <vendor>Foo project</vendor>
238  <vendor_url>https://foo.example.com</vendor_url>
239
240  <action id="com.example.foo.greet">
241    <_description>Good morning</_description>
242    <_message>Hello</_message>
243    <defaults>
244      <allow_active>yes</allow_active>
245    </defaults>
246  </action>
247</policyconfig>''')
248
249        self._mkpo()
250        (o, e, s) = self.do_install()
251        self.assertEqual(e, '')
252        self.assertEqual(s, 0)
253        self.assertNotIn('following files are not recognized', o)
254
255        f = self.installed_files()
256        self.assertIn('/usr/share/polkit-1/actions/com.example.foo.policy', f)
257        p = self._installed_contents('usr/share/polkit-1/actions/com.example.foo.policy')
258        self.assertIn('<description>Good morning</description>', p)
259        self.assertIn('<description xml:lang="de">Guten Morgen</description>', p)
260        self.assertIn('<message>Hello</message>', p)
261        self.assertIn('<message xml:lang="de">Hallo</message>', p)
262
263    def test_desktop(self):
264        '''*.desktop.in files'''
265
266        self._mksrc('gui/foogtk.desktop.in', '''[Desktop Entry]
267_Name=Hello
268_Comment=Good morning
269Exec=/bin/foo''')
270        self._mksrc('gui/autostart/fooapplet.desktop.in', '''[Desktop Entry]
271_Name=Hello
272_Comment=Good morning
273Exec=/usr/bin/fooapplet''')
274        self._mkpo()
275        self._mksrc('data/foosettings.desktop.in', '''[Desktop Entry]
276_Name=Hello
277_Comment=Good morning
278Exec=/bin/foosettings''')
279
280        (o, e, s) = self.do_install()
281        self.assertEqual(e, '')
282        self.assertEqual(s, 0)
283        self.assertNotIn('following files are not recognized', o)
284
285        f = self.installed_files()
286        self.assertIn('/usr/share/autostart/fooapplet.desktop', f)
287        self.assertIn('/usr/share/applications/foogtk.desktop', f)
288        self.assertIn('/usr/share/applications/foosettings.desktop', f)
289        # data/*.desktop.in shouldn't go to data dir
290        self.assertNotIn('/usr/share/foo/', f)
291
292        p = self._installed_contents('usr/share/autostart/fooapplet.desktop')
293        self.assertIn('\nName=Hello\n', p)
294        self.assertIn('\nName[de]=Hallo\n', p)
295        self.assertIn('\nComment[fr]=Bonjour\n', p)
296
297    def test_icons(self):
298        '''data/icons/'''
299
300        self._mksrc('data/icons/scalable/actions/press.png')
301        self._mksrc('data/icons/48x48/apps/foo.png')
302        scalable_icon_path = os.path.join(self.src, 'data', 'icons', 'scalable')
303        os.symlink(os.path.join(scalable_icon_path, 'actions', 'press.png'),
304                os.path.join(scalable_icon_path, 'actions', 'crunch.png'))
305
306        # test broken symlink, too
307        os.mkdir(os.path.join(scalable_icon_path, 'mimetypes'))
308        os.symlink('../apps/foo.svg',
309                os.path.join(scalable_icon_path, 'mimetypes', 'text-x-foo.svg'))
310
311        (o, e, s) = self.do_install()
312        self.assertEqual(e, '')
313        self.assertEqual(s, 0)
314        self.assertNotIn('following files are not recognized', o)
315
316        f = self.installed_files()
317        self.assertIn('/usr/share/icons/hicolor/scalable/actions/press.png', f)
318        self.assertIn('/usr/share/icons/hicolor/scalable/actions/crunch.png', f)
319        self.assertIn('/usr/share/icons/hicolor/48x48/apps/foo.png', f)
320        self.assertTrue(os.path.islink(os.path.join(self.install_tree,
321           'usr/share/icons/hicolor/scalable/actions/crunch.png')))
322        self.assertTrue(os.path.islink(os.path.join(self.install_tree,
323           'usr/share/icons/hicolor/scalable/mimetypes/text-x-foo.svg')))
324
325    def test_data(self):
326        '''Auxiliary files in data/'''
327
328        # have some explicitly covered files, to check that they don't get
329        # installed into prefix/share/foo/ again
330        self._mksrc('setup.py', '''
331from DistUtilsExtra.auto import setup
332from glob import glob
333
334setup(
335    name='foo',
336    version='0.1',
337    description='Test suite package',
338    url='https://foo.example.com',
339    license='GPL v2 or later',
340    author='Martin Pitt',
341    author_email='martin.pitt@example.com',
342
343    data_files = [
344      ('/lib/udev/rules.d', ['data/40-foo.rules']),
345      ('/etc/foo', glob('data/*.conf')),
346    ]
347)
348''')
349
350        self._mksrc('data/stuff')
351        self._mksrc('data/handlers/red.py', 'import sys\nprint ("RED")')
352        self._mksrc('data/handlers/blue.py', 'import sys\nprint ("BLUE")')
353        self._mksrc('data/40-foo.rules')
354        self._mksrc('data/blob1.conf')
355        self._mksrc('data/blob2.conf')
356        os.symlink('stuff', os.path.join(self.src, 'data', 'stufflink'))
357
358        (o, e, s) = self.do_install()
359        self.assertEqual(e, '')
360        self.assertEqual(s, 0)
361        self.assertNotIn('following files are not recognized', o)
362
363        f = self.installed_files()
364        self.assertIn('/usr/share/foo/stuff', f)
365        self.assertIn('/usr/share/foo/stufflink', f)
366        self.assertTrue(os.path.islink(os.path.join(self.install_tree, 'usr',
367            'share', 'foo', 'stufflink')))
368        self.assertIn('/usr/share/foo/handlers/red.py', f)
369        self.assertIn('/usr/share/foo/handlers/blue.py', f)
370        self.assertIn('/lib/udev/rules.d/40-foo.rules', f)
371        self.assertIn('/etc/foo/blob1.conf', f)
372        self.assertIn('/etc/foo/blob2.conf', f)
373        self.assertNotIn('/usr/share/foo/blob1.conf', f)
374        self.assertNotIn('/usr/share/foo/40-foo.rules', f)
375
376    def test_scripts(self):
377        '''scripts'''
378
379        # these should get autoinstalled
380        self._mksrc('bin/yell', '#!/bin/sh', True)
381        self._mksrc('bin/shout', '#!/bin/sh', True)
382        self._mksrc('bin/foo', b'#!/usr/bin/python\n# \xc2\xa9 copyright'.decode('UTF-8'), True)
383        os.symlink('shout', os.path.join(self.src, 'bin', 'shoutlink'))
384
385        # these shouldn't
386        self._mksrc('daemon/food', '#!/bin/sh', True) # not in bin/
387        self._mksrc('foob', '#!/bin/sh', True) # not named like project
388        # not executable
389        self._mksrc('bin/whisper', b'#!/usr/bin/python\n# \xc2\xa9 copyright'.decode('UTF-8'))
390
391        (o, e, s) = self.do_install()
392        self.assertEqual(e, '')
393        self.assertEqual(s, 0)
394        self.assertIn('following files are not recognized', o)
395        self.assertIn('\n  foob', o)
396        self.assertIn('\n  bin/whisper', o)
397        self.assertIn('\n  daemon/food', o)
398
399        f = self.installed_files()
400        self.assertIn('/usr/bin/yell', f)
401        self.assertIn('/usr/bin/shout', f)
402        self.assertIn('/usr/bin/shoutlink', f)
403        self.assertTrue(os.path.islink(os.path.join(self.install_tree, 'usr',
404            'bin', 'shoutlink')))
405        self.assertIn('/usr/bin/foo', f)
406        ftext = '\n'.join(f)
407        self.assertNotIn('food', ftext)
408        self.assertNotIn('foob', ftext)
409        self.assertNotIn('whisper', ftext)
410
411        # verify that they are executable
412        binpath = os.path.join(self.install_tree, 'usr', 'bin')
413        self.assertTrue(os.access(os.path.join(binpath, 'yell'), os.X_OK))
414        self.assertTrue(os.access(os.path.join(binpath, 'shout'), os.X_OK))
415        self.assertTrue(os.access(os.path.join(binpath, 'foo'), os.X_OK))
416
417    def test_pot_manual(self):
418        '''PO template creation with manual POTFILES.in'''
419
420        self._mk_i18n_source()
421        self._mksrc('po/foo.pot', '')
422        # only do a subset here
423        self._mksrc('po/POTFILES.in', '''
424gtk/main.py
425gui/foo.desktop.in
426[type: gettext/glade]gtk/test.ui''')
427
428        (o, e, s) = self.setup_py(['build'])
429        self.assertEqual(e, '')
430        self.assertEqual(s, 0)
431        # POT file should not be shown as not recognized
432        self.assertNotIn('\n  po/foo.pot\n', o)
433
434        pot = self._src_contents('po/foo.pot')
435
436        self.assertNotIn('msgid "no"', pot)
437        self.assertIn('msgid "yes1"', pot)
438        self.assertIn('msgid "yes2 %s"', pot)
439        self.assertNotIn('msgid "yes5"', pot) # we didn't add helpers.py
440        self.assertIn('msgid "yes7"', pot) # we did include the desktop file
441        self.assertNotIn('msgid "yes5"', pot) # we didn't add helpers.py
442        self.assertIn('msgid "yes11"', pot) # we added one GTKBuilder file
443        self.assertNotIn('msgid "yes12"', pot) # ... but not the other
444
445    def test_pot_auto(self):
446        '''PO template creation with automatic POTFILES.in'''
447
448        self._mk_i18n_source()
449
450        (o, e, s) = self.setup_py(['build'])
451        self.assertEqual(e, '')
452        self.assertEqual(s, 0)
453        # POT file should not be shown as not recognized
454        self.assertNotIn('\n  po/foo.pot\n', o)
455
456        pot = self._src_contents('po/foo.pot')
457
458        self.assertNotIn('msgid "no"', pot)
459        for i in range(2, 15):
460            self.assertTrue('msgid "yes%i' % i in pot or
461                   'msgid ""\n"yes%i' % i in pot,
462                   'yes%i' % i)
463        # above loop would match yes11 to yes1 as well, so test it explicitly
464        self.assertIn('msgid "yes1"', pot)
465
466    def test_pot_auto_explicit(self):
467        '''PO template creation with automatic POTFILES.in and explicit scripts'''
468
469        self._mk_i18n_source()
470
471        # add some additional binaries here which aren't caught by default
472        self._mksrc('cli/client-cli', "#!/usr/bin/python\nprint (_('yes15'))", True)
473        self._mksrc('gtk/client-gtk', '#!/usr/bin/python\nprint (_("yes16"))', True)
474        # this is the most tricky case: intltool doesn't consider them Python
475        # files by default and thus just looks for _(""):
476        self._mksrc('kde/client-kde', "#!/usr/bin/python\nprint (_('yes17'))", True)
477        self._mksrc('po/POTFILES.in.in', 'gtk/client-gtk\nkde/client-kde')
478        self._mksrc('setup.py', '''
479from DistUtilsExtra.auto import setup
480
481import warnings
482warnings.filterwarnings('ignore', 'pipe2 set errno ENOSYS.*')
483
484setup(
485    name='foo',
486    version='0.1',
487    data_files=[('share/foo', ['gtk/client-gtk', 'kde/client-kde'])],
488    scripts=['cli/client-cli'],
489)
490''')
491
492        (o, e, s) = self.setup_py(['build'])
493        self.assertEqual(e, '')
494        self.assertEqual(s, 0)
495        # POT file should not be shown as not recognized
496        self.assertNotIn('\n  po/foo.pot\n', o)
497
498        pot = self._src_contents('po/foo.pot')
499
500        self.assertNotIn('msgid "no"', pot)
501        for i in range(2, 18):
502            self.assertTrue('msgid "yes%i' % i in pot or
503                   'msgid ""\n"yes%i' % i in pot,
504                   'yes%i' % i)
505        # above loop would match yes11 to yes1 as well, so test it explicitly
506        self.assertIn('msgid "yes1"', pot)
507
508    def test_standard_files(self):
509        '''Standard files (MANIFEST.in, COPYING, etc.)'''
510
511        self._mksrc('AUTHORS')
512        self._mksrc('COPYING')
513        self._mksrc('LICENSE')
514        self._mksrc('COPYING.LIB')
515        self._mksrc('README.txt')
516        self._mksrc('MANIFEST.in')
517        self._mksrc('MANIFEST')
518        self._mksrc('NEWS')
519        self._mksrc('TODO')
520
521        (o, e, s) = self.do_install()
522        self.assertEqual(e, '')
523        self.assertEqual(s, 0)
524        self.assertNotIn('following files are not recognized', o)
525
526        f = self.installed_files()
527        self.assertIn('/usr/share/doc/foo/README.txt', f)
528        self.assertIn('/usr/share/doc/foo/NEWS', f)
529        ftext = '\n'.join(f)
530        self.assertNotIn('MANIFEST', ftext)
531        self.assertNotIn('COPYING', ftext)
532        self.assertNotIn('COPYING', ftext)
533        self.assertNotIn('AUTHORS', ftext)
534        self.assertNotIn('TODO', ftext)
535
536        # sub-dir READMEs shouldn't be installed by default
537        self.snapshot = None
538        self._mksrc('extra/README')
539        (o, e, s) = self.do_install()
540        self.assertEqual(e, '')
541        self.assertEqual(s, 0)
542        self.assertIn('following files are not recognized', o)
543        self.assertIn('\n  extra/README\n', o)
544
545    def test_sdist(self):
546        '''default MANIFEST'''
547
548        good = ['AUTHORS', 'README.txt', 'COPYING', 'helpers.py',
549                'foo/__init__.py', 'foo/bar.py', 'tests/all.py',
550                'gui/x.desktop.in', 'backend/foo.policy.in',
551                'daemon/backend.conf', 'x/y', 'po/de.po', 'po/foo.pot',
552                '.quickly', 'data/icons/16x16/apps/foo.png', 'bin/foo',
553                'backend/food', 'backend/com.example.foo.service',
554                'gtk/main.glade', 'dist/extra.tar.gz']
555        bad = ['po/de.mo', '.helpers.py.swp', '.bzr/index', '.svn/index',
556               '.git/index', 'bin/foo~', 'backend/foo.pyc',
557               'dist/foo-0.1.tar.gz', '.shelf/1', '.bzr/revs', '.git/config']
558
559        for f in good + bad:
560            self._mksrc(f)
561
562        (o, e, s) = self.setup_py(['sdist', '-o'])
563        self.assertIn("'MANIFEST.in' does not exist", e)
564        self.assertEqual(s, 0)
565
566        manifest = self._src_contents('MANIFEST').splitlines()
567
568        for f in good:
569            self.assertIn(f, manifest)
570        for f in bad:
571            self.assertNotIn(f, manifest)
572        os.unlink(os.path.join(self.src, 'MANIFEST'))
573
574    def test_ui(self):
575        '''GtkBuilder/Qt *.ui'''
576
577        self._mksrc('gtk/test.ui', b'''<?xml version="1.0"?>
578<interface>
579  <requires lib="gtk+" version="2.16"/>
580  <object class="GtkWindow" id="window1">
581    <property name="title" translatable="yes">my\xe2\x99\xa5</property>
582    <child><placeholder/></child>
583  </object>
584</interface>'''.decode('UTF-8'))
585
586        self._mksrc('gtk/settings.ui', '''<?xml version="1.0"?>
587<!-- Generated with glade 3.18.3 -->
588<interface domain="foobar">
589  <requires lib="gtk+" version="2.16"/>
590  <object class="GtkWindow" id="window2">
591    <property name="title" translatable="yes">yes12</property>
592    <child><placeholder/></child>
593  </object>
594</interface>''')
595
596        self._mksrc('kde/mainwindow.ui', '''<?xml version="1.0"?>
597<ui version="4.0">
598 <class>CrashDialog</class>
599 <widget class="QDialog" name="CrashDialog">
600 </widget>
601</ui>
602''')
603
604        self._mksrc('someweird.ui')
605
606        (o, e, s) = self.do_install()
607        self.assertEqual(e, '')
608        self.assertEqual(s, 0)
609        self.assertIn('following files are not recognized', o)
610        self.assertIn('\n  someweird.ui\n', o)
611
612        f = self.installed_files()
613        self.assertIn('/usr/share/foo/test.ui', f)
614        self.assertIn('/usr/share/foo/settings.ui', f)
615        self.assertIn('/usr/share/foo/mainwindow.ui', f)
616        ftext = '\n'.join(f)
617        self.assertNotIn('someweird', ftext)
618
619    def test_manpages(self):
620        '''manpages'''
621
622        self._mksrc('man/foo.1', '.TH foo 1 "Jan 01, 1900" "Joe Developer"')
623        self._mksrc('daemon/food.8', '.\" some comment\n.TH food 8 "Jan 01, 1900" "Joe Developer"')
624        self._mksrc('cruft/food.1', '')
625        self._mksrc('daemon/notme.s', '.TH food 8 "Jan 01, 1900" "Joe Developer"')
626
627        (o, e, s) = self.do_install()
628        self.assertEqual(e, '')
629        self.assertEqual(s, 0)
630        self.assertIn('following files are not recognized', o)
631        self.assertIn('\n  cruft/food.1\n', o)
632        self.assertIn('\n  daemon/notme.s\n', o)
633
634        f = self.installed_files()
635        self.assertIn('/usr/share/man/man1/foo.1', f)
636        self.assertIn('/usr/share/man/man8/food.8', f)
637        ftext = '\n'.join(f)
638        self.assertNotIn('food.1', ftext)
639        self.assertNotIn('notme', ftext)
640
641    def test_etc(self):
642        '''etc/*'''
643
644        self._mksrc('etc/cron.daily/foo')
645        self._mksrc('etc/foo.conf')
646        self._mksrc('etc/init.d/foo', executable=True)
647        d = os.path.join(self.src, 'etc', 'cron.weekly')
648        os.mkdir(d)
649        os.symlink(os.path.join('..', 'cron.daily', 'foo'),
650                os.path.join(d, 'foo'))
651
652        (o, e, s) = self.do_install()
653        self.assertEqual(e, '')
654        self.assertEqual(s, 0)
655        self.assertNotIn('following files are not recognized', o)
656
657        f = self.installed_files()
658        self.assertIn('/etc/cron.daily/foo', f)
659        self.assertIn('/etc/cron.weekly/foo', f)
660        self.assertIn('/etc/init.d/foo', f)
661        self.assertIn('/etc/foo.conf', f)
662
663        # verify that init script is executable
664        self.assertTrue(os.access(os.path.join(self.install_tree, 'etc', 'init.d',
665            'foo'), os.X_OK))
666        # verify that symlinks get preserved
667        self.assertTrue(os.path.islink(os.path.join(self.install_tree, 'etc',
668            'cron.weekly', 'foo')))
669
670        # check that we can install again into the same source tree
671        (o, e, s) = self.setup_py(['install', '--no-compile', '--prefix=/usr',
672            '--root=' + self.install_tree])
673        self.assertEqual(e, '')
674        self.assertEqual(s, 0)
675        self.assertNotIn('following files are not recognized', o)
676
677    def test_requires_provides(self):
678        '''automatic requires/provides'''
679
680        for needed_pkg in ['pkg_resources','httplib2','gi.repository.GLib']:
681            try:
682                __import__(needed_pkg)
683            except ImportError:
684                self.fail('You need to have %s installed for this test suite to work' % needed_pkg)
685
686        self._mksrc('foo/__init__.py', '')
687        self._mksrc('foo/stuff.py', '''import xml.parsers.expat
688import os, os.path, email.mime, distutils.command.register
689from email import header as h
690import httplib2.iri2uri, unknown
691from . bar import poke
692from bar.poke import x
693import grab_cli
694import broken
695''')
696
697        self._mksrc('foo/bar/__init__.py', '')
698        self._mksrc('foo/bar/poke.py', 'from . import broken\ndef x(): pass')
699        self._mksrc('foo/bar/broken.py', 'raise RuntimeError("cannot initialize system")')
700
701        self._mksrc('mymod.py', 'import foo\nfrom foo.bar.poke import x')
702        # trying to import this will cause setup.py to not process any args any more
703        self._mksrc('grab_cli.py', 'from optparse import OptionParser\nOptionParser().parse_args()')
704        # trying to import this will break setup.py
705        self._mksrc('broken.py', 'raise SystemError("cannot initialize system")')
706        self._mksrc('pygi.py', 'from gi.repository import GLib\nimport gi.repository.GObject')
707
708        self._mksrc('bin/foo-cli', '''#!/usr/bin/python
709import sys
710import pkg_resources
711import foo.bar
712from httplib2 import iri2uri
713
714print ('import iamnota.module')
715''', executable=True)
716
717        # this shouldn't be treated specially
718        self._mksrc('data/example-code/template.py', 'import example.module')
719        self._mksrc('data/example-code/mymod/__init__.py', '')
720        self._mksrc('data/example-code/mymod/shiny.py', 'import example.othermod')
721
722        (o, e, s) = self.do_install()
723        self.assertEqual(s, 0, e)
724        self.assertEqual(e, 'ERROR: Python module unknown not found\n')
725        self.assertNotIn('following files are not recognized', o)
726
727        inst = self.installed_files()
728        self.assertIn('/usr/share/foo/example-code/template.py', inst)
729        self.assertIn('/usr/share/foo/example-code/mymod/shiny.py', inst)
730        for f in inst:
731            if 'template.py' in f or 'shiny' in f:
732                self.assertNotIn('packages', f)
733
734        # parse .egg-info
735        (o, e, s) = self.setup_py(['install_egg_info', '-d', self.install_tree])
736        self.assertEqual(e, 'ERROR: Python module unknown not found\n')
737        egg_paths = [x for x in inst if x.endswith('.egg-info')]
738        self.assertEqual(len(egg_paths), 1)
739        egg = self._installed_contents(egg_paths[0].strip(os.path.sep)).splitlines()
740        self.assertIn('Name: foo', egg)
741
742        # check provides
743        prov = [prop.split(' ', 1)[1] for prop in egg if prop.startswith('Provides: ')]
744        self.assertEqual(set(prov), set(['foo', 'mymod', 'broken', 'grab_cli', 'pygi']))
745
746        # check requires
747        req = [prop.split(' ', 1)[1] for prop in egg if prop.startswith('Requires: ')]
748        self.assertEqual(set(req), set(['httplib2', 'pkg_resources',
749            'gi.repository.GLib', 'gi.repository.GObject']))
750
751    def test_help_docbook(self):
752        '''Docbook XML help'''
753
754        self._mksrc('help/C/index.docbook')
755        self._mksrc('help/C/legal.xml')
756        self._mksrc('help/C/figures/mainscreen.png')
757        self._mksrc('help/de/index.docbook')
758        self._mksrc('help/de/legal.xml')
759        self._mksrc('help/de/figures/mainscreen.png')
760
761        self._mksrc('help/weird.xml')
762        self._mksrc('help/notme.png')
763
764        (o, e, s) = self.do_install()
765        self.assertEqual(e, '')
766        self.assertEqual(s, 0)
767        self.assertIn('following files are not recognized', o)
768        self.assertIn('\n  help/weird.xml\n', o)
769        self.assertIn('\n  help/notme.png\n', o)
770
771        f = self.installed_files()
772        self.assertIn('/usr/share/help/C/foo/index.docbook', f)
773        self.assertIn('/usr/share/help/C/foo/legal.xml', f)
774        self.assertIn('/usr/share/help/C/foo/figures/mainscreen.png', f)
775        self.assertIn('/usr/share/help/de/foo/index.docbook', f)
776        self.assertIn('/usr/share/help/de/foo/legal.xml', f)
777        self.assertIn('/usr/share/help/de/foo/figures/mainscreen.png', f)
778
779    def test_help_mallard(self):
780        '''Mallard XML help'''
781
782        self._mksrc('help/C/index.page')
783        self._mksrc('help/C/legal.page')
784        self._mksrc('help/C/figures/mainscreen.png')
785        self._mksrc('help/de/index.page')
786        self._mksrc('help/de/legal.page')
787        self._mksrc('help/de/figures/mainscreen.png')
788
789        self._mksrc('help/weird.page')
790        self._mksrc('help/notme.png')
791
792        (o, e, s) = self.do_install()
793        self.assertEqual(e, '')
794        self.assertEqual(s, 0)
795        self.assertIn('following files are not recognized', o)
796        self.assertIn('\n  help/weird.page\n', o)
797        self.assertIn('\n  help/notme.png\n', o)
798
799        f = self.installed_files()
800        self.assertIn('/usr/share/help/C/foo/index.page', f)
801        self.assertIn('/usr/share/help/C/foo/legal.page', f)
802        self.assertIn('/usr/share/help/C/foo/figures/mainscreen.png', f)
803        self.assertIn('/usr/share/help/de/foo/index.page', f)
804        self.assertIn('/usr/share/help/de/foo/legal.page', f)
805        self.assertIn('/usr/share/help/de/foo/figures/mainscreen.png', f)
806
807    def test_binary_files(self):
808        '''Binary files are ignored'''
809
810        with open(os.path.join(self.src, 'binary_trap'), 'wb') as f:
811            f.write(b'\x00\x01abc\xFF\xFE')
812        (o, e, s) = self.do_install()
813        self.assertEqual(e, '')
814        self.assertEqual(s, 0)
815        self.assertIn('following files are not recognized', o)
816        self.assertIn('\n  binary_trap\n', o)
817
818        f = self.installed_files()
819        self.assertEqual(len(f), 1, f)
820        self.assertIn('egg-info', f[0])
821
822    def test_utf8_filenames(self):
823        '''UTF-8 file names'''
824
825        bin_fname = b'a\xc3\xa4b.bin'.decode('UTF-8')
826        with open(os.path.join(self.src, bin_fname).encode('UTF-8'), 'wb') as f:
827            f.write(b'\x00\x01abc\xFF\xFE')
828
829        (o, e, s) = self.do_install()
830        self.assertEqual(e, '')
831        self.assertEqual(s, 0)
832
833        f = self.installed_files()
834        self.assertEqual(len(f), 1, f)
835        self.assertIn('egg-info', f[0])
836
837        self.assertIn('following files are not recognized', o)
838        # this might not be the correct file name when the locale is e. g. C
839        self.assertIn('b.bin\n', o)
840
841    #
842    # helper methods
843    #
844
845    def setup_py(self, args):
846        '''Run setup.py with given arguments.
847
848        For convenience, this snapshots the tree if no snapshot exists yet.
849
850        Return (out, err, exitcode) triple.
851        '''
852        if not self.snapshot:
853            self.do_snapshot()
854
855        env = os.environ.copy()
856        oldcwd = os.getcwd()
857        if 'PYTHONPATH' in env:
858            env['PYTHONPATH'] = oldcwd + os.pathsep + env['PYTHONPATH']
859        else:
860            env['PYTHONPATH'] = oldcwd
861        # unset envvars that alter results
862        env.pop('LINGUAS', '')
863        env.pop('PYTHONDONTWRITEBYTECODE', '')
864        os.chdir(self.src)
865        s = subprocess.Popen(['/proc/self/exe', 'setup.py'] + args, env=env,
866            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
867        (out, err) = s.communicate()
868        out = out.decode()
869        err = err.decode()
870        os.chdir(oldcwd)
871
872        return (out, err, s.returncode)
873
874    def do_install(self):
875        '''Run setup.py install into temporary tree.
876
877        Return (out, err, exitcode) triple.
878        '''
879        self.install_tree = tempfile.mkdtemp()
880
881        self.setup_py(['build'])
882        return self.setup_py(['install', '--no-compile', '--skip-build',
883            '--prefix=/usr', '--root=' + self.install_tree])
884
885    def installed_files(self):
886        '''Return list of file paths in install tree.'''
887
888        result = []
889        for root, _, files in os.walk(self.install_tree):
890            assert root.startswith(self.install_tree)
891            r = root[len(self.install_tree):]
892            for f in files:
893                result.append(os.path.join(r, f))
894        return result
895
896    def _mksrc(self, path, content=None, executable=False):
897        '''Create a file in the test source tree.'''
898
899        path = os.path.join(self.src, path)
900        dir = os.path.dirname(path)
901        if not os.path.isdir(dir):
902            os.makedirs(dir)
903        with open(path, 'wb') as f:
904            if content is None:
905                # default content, to spot with diff
906                f.write(b'dummy')
907            else:
908                f.write((content + '\n').encode('UTF-8'))
909
910        if executable:
911            os.chmod(path, 0o755)
912
913    def do_snapshot(self):
914        '''Snapshot source tree.
915
916        This should be called after a test set up all source files.
917        '''
918        assert self.snapshot is None, 'snapshot already taken'
919
920        self.snapshot = tempfile.mkdtemp()
921        shutil.copytree(self.src, os.path.join(self.snapshot, 's'), symlinks=True)
922
923    def diff_snapshot(self):
924        '''Compare source tree to snapshot.
925
926        Return diff -Nur output.
927        '''
928        assert self.snapshot, 'no snapshot taken'
929        diff = subprocess.Popen(['diff', '-x', 'foo.pot', '-x', '*.pyc',
930            '-Nur', os.path.join(self.snapshot, 's'), self.src],
931            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
932        (out, err) = diff.communicate()
933        out = out.decode('UTF-8')
934        return out
935
936    def _mkpo(self):
937        '''Create some example po files.'''
938
939        self._mksrc('po/POTFILES.in', '')
940        self._mksrc('po/de.po', '''msgid ""
941msgstr "Content-Type: text/plain; charset=UTF-8\\n"
942
943msgid "Good morning"
944msgstr "Guten Morgen"
945
946msgid "Hello"
947msgstr "Hallo"''')
948        self._mksrc('po/fr.po', '''msgid ""
949msgstr "Content-Type: text/plain; charset=UTF-8\\n"
950
951msgid "Good morning"
952msgstr "Bonjour"''')
953
954    def _mk_i18n_source(self):
955        '''Create some example source files with gettext calls'''
956
957        self._mksrc('gtk/main.py', '''print (_("yes1"))
958print ("no1")
959print (__("no2"))
960x = _('yes2 %s') % y
961
962def f():
963    print (_("yes3"))
964    return _('yes6')''')
965
966        self._mksrc('helpers.py', '''
967print (f(_("yes4")))
968print (_(\'\'\'yes5
969even more
970lines\'\'\'))
971print (_("""yes6
972more lines"""))
973print (\'\'\'no3
974boo\'\'\')
975print ("""no4
976more""")''')
977
978        self._mksrc('gui/foo.desktop.in', '''[Desktop Entry]
979_Name=yes7
980_Comment=yes8
981Icon=no5
982Exec=/usr/bin/foo''')
983
984        self._mksrc('daemon/com.example.foo.policy.in', '''<?xml version="1.0" encoding="UTF-8"?>
985<!DOCTYPE policyconfig PUBLIC
986 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
987 "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
988<policyconfig>
989  <action id="com.example.foo.greet">
990    <_description>yes9</_description>
991    <_message>yes10</_message>
992    <defaults>
993      <allow_active>no6</allow_active>
994    </defaults>
995  </action>
996</policyconfig>''')
997
998        self._mksrc('gtk/test.ui', '''<?xml version="1.0"?>
999<interface>
1000  <requires lib="gtk+" version="2.16"/>
1001  <object class="GtkWindow" id="window1">
1002    <property name="title" translatable="yes">yes11</property>
1003    <child><placeholder/></child>
1004  </object>
1005</interface>''')
1006
1007        self._mksrc('data/settings.ui', '''<?xml version="1.0"?>
1008<interface domain="foobar">
1009  <requires lib="gtk+" version="2.16"/>
1010  <object class="GtkWindow" id="window1">
1011    <property name="title" translatable="yes">yes12</property>
1012    <child><placeholder/></child>
1013  </object>
1014</interface>''')
1015
1016        self._mksrc('Makefile', 'echo _("no7")')
1017
1018        # Executables without *.py extension
1019        self._mksrc('gtk/foo-gtk', '#!/usr/bin/python\nprint (_("yes13"))',
1020                executable=True)
1021        self._mksrc('cli/foo-cli', '#!/usr/bin/env python\nprint (_(\'yes14\'))',
1022                executable=True)
1023        self._mksrc('daemon/foobarize', '#!/usr/bin/flex\np _("no8")',
1024                executable=True)
1025
1026    def _src_contents(self, path):
1027        f = open(os.path.join(self.src, path))
1028        contents = f.read()
1029        f.close()
1030        return contents
1031
1032    def _installed_contents(self, path):
1033        f = open(os.path.join(self.install_tree, path))
1034        contents = f.read()
1035        f.close()
1036        return contents
1037
1038
1039if __name__ == '__main__':
1040    unittest.main()
1041