Do not use data_files in setup.py
[stgit.git] / contrib / release / pkgtest.py
blob42740032bd10db2355371fb780b1b674089b9876
1 #!/usr/bin/env python3
2 """Test various aspects of StGit packaging.
4 [x] Use XDG_RUNTIME_DIR
5 [ ] Use chroot
6 [x] Use clone of repo
7 [ ] Make annotated tags within test repo
8 [x] Different python versions
9 [ ] Different setuptools versions
11 Test source distribution creation:
12 [x] Ensure generated files are in sdist tarball
13 [x] Ensure that generated files are not in worktree
14 [x] Ensure dirty worktree is reflected in version
16 Test build:
17 [x] Ensure generated python files land in build/
18 [x] Ensure generated python files also land in worktree
19 [x] Ensure generated completions land in worktree
21 Test install from git worktree:
22 [x] Test develop mode
23 [x] Test installing to virtualenv
24 [ ] Test installing to system (i.e. w/chroot)
26 Test install from unpacked sdist tarball
27 [x] Test installing to virtualenv
28 [ ] Test installing to system (i.e. w/chroot)
30 [x] Test install from git URL
32 Test install sdist tarball with pip
33 [x] Ensure generated python is in right place
34 [x] Ensure generated completions in right place
35 [x] Ensure other files in right places
37 Test making sdist from sdist
38 [x] Make sdist from git worktree
39 [x] Unpack sdist
40 [x] Make sdist from unpacked sdist tree
41 [x] Ensure identical tarballs
43 Test wheels
44 [x] Test build with bdist_wheel
45 [x] Test install whl to virtualenv
47 Test git archives
48 [x] Test generate git archive
49 [x] Test make sdist from git archive
50 [ ] Test install from unpacked git archive
52 Test running test suite
53 [x] from sdist
54 [ ] from git archive
55 [ ] with installed stg
57 """
59 import argparse
60 import os
61 import shutil
62 import subprocess
63 import sys
64 import tarfile
65 import zipfile
66 from pathlib import Path
68 PYTHON_VERSIONS = [
69 'python3.9',
70 'python3.8',
71 'python3.7',
72 'python3.6',
73 'python3.5',
74 'pypy3',
77 BUILD_PACKAGE_NAMES = ['pip', 'wheel', 'setuptools']
80 def log(*args):
81 print(':::', *args)
84 def log_test(testname, *args):
85 print('\n###', testname, *args)
88 def printable_cmd(cmd_list):
89 runtime_dir = Path(os.environ['XDG_RUNTIME_DIR'])
90 home = Path.home()
91 printable_items = []
92 for item in cmd_list:
93 if isinstance(item, Path):
94 if item.is_relative_to(runtime_dir):
95 printable = f'{item.relative_to(runtime_dir)}'
96 elif item.is_relative_to(home):
97 printable = f'~/{item.relative_to(home)}'
98 else:
99 printable = f'{item}'
100 printable_items.append(printable)
101 else:
102 printable_items.append(item)
103 return ' '.join(printable_items)
106 def subprocess_cmd(cmd_list):
107 norm_cmd = []
108 for item in cmd_list:
109 if isinstance(item, Path):
110 norm_cmd.append(str(item))
111 else:
112 norm_cmd.append(item)
113 return norm_cmd
116 def call(cmd, cwd, **kwargs):
117 print('!!!', printable_cmd(cmd), f'(in {printable_cmd([cwd])})')
118 stdout = kwargs.get('stdout')
119 if stdout is None:
120 proc = subprocess.run(
121 subprocess_cmd(cmd),
122 cwd=cwd,
123 stdout=subprocess.PIPE,
124 stderr=subprocess.STDOUT,
126 for line in proc.stdout.decode().splitlines():
127 print('...', line)
128 return proc.returncode
129 else:
130 return subprocess.call(subprocess_cmd(cmd), cwd=cwd, **kwargs)
133 def check_call(cmd, cwd, **kwargs):
134 print('!!!', printable_cmd(cmd), f'(in {printable_cmd([cwd])})')
135 stdout = kwargs.get('stdout')
136 if stdout is None:
137 proc = subprocess.run(
138 subprocess_cmd(cmd),
139 cwd=cwd,
140 stdout=subprocess.PIPE,
141 stderr=subprocess.STDOUT,
143 for line in proc.stdout.decode().splitlines():
144 print('...', line)
145 proc.check_returncode()
146 else:
147 subprocess.check_call(subprocess_cmd(cmd), cwd=cwd, **kwargs)
150 def check_output(cmd, cwd):
151 print('!!!', printable_cmd(cmd), f'(in {printable_cmd([cwd])})')
152 return subprocess.check_output(subprocess_cmd(cmd), cwd=cwd)
155 def main():
156 parser = argparse.ArgumentParser()
157 parser.set_defaults(pythons=[])
158 for pyver in PYTHON_VERSIONS:
159 parser.add_argument(
160 f'--{pyver}', dest='pythons', action='append_const', const=pyver
162 parser.add_argument('--all-pythons', action='store_true')
163 args = parser.parse_args()
165 if args.all_pythons:
166 args.pythons[:] = PYTHON_VERSIONS
167 elif not args.pythons:
168 args.pythons.append(PYTHON_VERSIONS[0])
170 assert 'VIRTUAL_ENV' not in os.environ, 'Do not run this from a virtual env'
172 system_stg = shutil.which('stg')
173 log(f'{system_stg=}')
174 if system_stg is None:
175 system_stg_version = None
176 else:
177 system_stg_version = version_from_stg_version(
178 check_output(['stg', '--version'], cwd=Path('/')).decode()
180 log(f'{system_stg_version=}')
182 this_repo = Path(
183 check_output(['git', 'rev-parse', '--show-toplevel'], cwd=Path())
184 .rstrip()
185 .decode()
187 log(f'{this_repo=}')
188 assert this_repo.is_absolute()
190 runtime_dir = Path(os.environ['XDG_RUNTIME_DIR'])
191 log(f'{runtime_dir=}')
192 test_root = runtime_dir / 'stgit-pkgtest'
193 log(f'{test_root=}')
194 assert test_root.is_absolute()
195 if test_root.exists():
196 shutil.rmtree(test_root)
197 test_root.mkdir(exist_ok=False)
199 cache_root = runtime_dir / 'stgit-pkgcache'
200 should_create_cache = not cache_root.exists()
201 log(f'{cache_root=} {should_create_cache=}')
202 assert cache_root.is_absolute()
204 if should_create_cache:
205 create_cache(cache_root)
207 for python in args.pythons:
208 base = test_root / python
209 repo = prepare_test_repo(base, this_repo)
210 prepare_virtual_env(base, python, cache_root)
211 test_dirty_version(base, repo)
212 test_install_from_git_url(base, repo, cache_root)
213 test_uninstall_from_venv(base)
214 test_sdist_creation(base, repo)
215 test_create_git_archive(base, repo)
216 test_build(base, repo)
217 test_bdist_wheel(base, repo)
218 test_develop_mode(base, repo, cache_root)
219 test_uninstall_from_venv(base)
220 test_running_tests_from_sdist(base)
221 test_install_from_unpacked_sdist(base, cache_root)
222 test_uninstall_from_venv(base)
223 test_install_sdist(base, cache_root)
224 test_uninstall_from_venv(base)
225 test_install_wheel(base)
226 test_uninstall_from_venv(base)
229 def venv_py_exe(base):
230 return base / 'venv' / 'bin' / 'python'
233 def venv_stg_exe(base):
234 return base / 'venv' / 'bin' / 'stg'
237 def version_from_sdist(tgz_path):
238 return tgz_path.name.split('stgit-', 1)[1].rsplit('.tar.gz', 1)[0]
241 def version_from_wheel(whl_path):
242 return whl_path.name.split('stgit-', 1)[1].rsplit('-py3-none-any.whl')[0]
245 def version_from_stg_version(output):
246 for line in output.splitlines():
247 if line.lower().startswith('stacked git'):
248 return line[12:]
251 def create_cache(cache_root):
252 log_test('create_cache')
253 cache_root.mkdir()
254 check_call(
255 [sys.executable, '-m', 'pip', 'download'] + BUILD_PACKAGE_NAMES,
256 cwd=cache_root,
259 for package_name in BUILD_PACKAGE_NAMES:
260 wheel_paths = list(cache_root.glob(f'{package_name}-*.whl'))
261 assert len(wheel_paths) == 1, wheel_paths
262 wheel_path = wheel_paths[0]
263 wheel_link = cache_root / f'{package_name}.whl'
264 wheel_link.symlink_to(wheel_path)
267 def prepare_test_repo(base, this_repo):
268 log_test('prepare_test_repo')
269 test_repo = base / 'stgit-git'
270 check_call(
271 ['git', 'clone', '--quiet', '--local', '--no-hardlinks', this_repo, test_repo],
272 cwd=this_repo,
274 log(f'{test_repo=}')
275 return test_repo
278 def prepare_virtual_env(base, python, cache_root):
279 log_test('prepare_virtual_env')
280 venv_path = base / 'venv'
281 check_call([python, '-m', 'venv', venv_path], cwd=base)
282 packages = [
283 (cache_root / f'{package_name}.whl').resolve()
284 for package_name in BUILD_PACKAGE_NAMES
286 check_call(
287 [venv_py_exe(base), '-m', 'pip', 'install', '--no-index'] + packages,
288 cwd=cache_root,
290 check_call(
291 [venv_py_exe(base), '-m', 'pip', 'list', '--no-index', '--format=freeze'],
292 cwd=base,
294 log(f'{venv_path=}')
295 return venv_path
298 def test_dirty_version(base, repo):
299 log_test('test_dirty_version')
301 # Modify a checked-in file to make worktree dirty
302 dirty_file = repo / 'README.md'
303 with open(dirty_file, 'a') as f:
304 print("DIRTY STUFF", file=f)
306 # Make sdist from dirty repo
307 check_call(
308 [venv_py_exe(base), 'setup.py', 'sdist'],
309 cwd=repo,
310 stdout=subprocess.DEVNULL,
312 sdist_paths = list((repo / 'dist').glob('stgit-*.tar.gz'))
313 assert len(sdist_paths) == 1
314 stgit_tgz = sdist_paths[0]
315 log(f'{stgit_tgz=}')
317 # Ensure sdist's version is marked 'dirty'
318 version = version_from_sdist(stgit_tgz)
319 log(f'{version=}')
320 assert 'dirty' in version
322 # Restore worktree to pristine state
323 check_call(['git', 'checkout', '--', dirty_file], cwd=repo)
324 check_call(['git', 'diff', '--quiet'], cwd=repo)
325 check_call(['git', 'clean', '-qfx'], cwd=repo)
328 def test_install_from_git_url(base, repo, cache):
329 log_test('test_install_from_git_url')
330 check_call(
332 venv_py_exe(base),
333 '-m',
334 'pip',
335 'install',
336 # '--no-clean',
337 '--no-index',
338 '--find-links',
339 cache,
340 f'git+file://{str(repo)}',
342 cwd=base,
344 version = version_from_stg_version(
345 check_output([venv_py_exe(base), '-m', 'stgit', '--version'], cwd=base).decode()
347 log(f'{version=}')
349 # Ensure cmdlist.py was generated
350 check_call([venv_py_exe(base), '-c', 'import stgit.commands.cmdlist'], cwd=base)
353 def test_sdist_creation(base, repo):
354 log_test('test_sdist_creation')
355 dist = base / 'dist'
356 check_call(
357 [venv_py_exe(base), 'setup.py', 'sdist', '--dist-dir', dist],
358 cwd=repo,
359 stdout=subprocess.DEVNULL,
361 sdist_paths = list(dist.glob('stgit-*.tar.gz'))
362 assert len(sdist_paths) == 1
363 stgit_tgz = sdist_paths[0]
364 log(f'{stgit_tgz=}')
365 prefix = Path(stgit_tgz.with_suffix('').with_suffix('').name)
366 version = version_from_sdist(stgit_tgz)
367 log(f'{version=}')
369 # Ensure generated files are in sdist tarball
370 with tarfile.open(stgit_tgz) as tf:
371 tf.getmember(str(prefix / 'completion' / 'stg.fish'))
372 tf.getmember(str(prefix / 'completion' / 'stgit.bash'))
373 tf.getmember(str(prefix / 'stgit' / 'commands' / 'cmdlist.py'))
374 _version_tar_info = tf.getmember(str(prefix / 'stgit' / '_version.py'))
375 assert 'def _get_version()' not in _version_tar_info.tobuf().decode()
377 # Ensure that no checked-in files in the working tree were altered
378 check_call(['git', 'diff', '--quiet'], cwd=repo)
380 # Extract existing sdist from base/dist
381 orig_sdist = base / 'orig-sdist'
382 orig_sdist.mkdir()
383 work_sdist = base / 'work-sdist'
384 work_sdist.mkdir()
385 with tarfile.open(stgit_tgz) as tf:
386 tf.extractall(orig_sdist)
387 tf.extractall(work_sdist)
389 # Create an sdist from an sdist
390 dist2 = base / 'dist2'
391 check_call(
392 [venv_py_exe(base), 'setup.py', 'sdist', '--dist-dir', dist2],
393 cwd=work_sdist / prefix,
394 stdout=subprocess.DEVNULL,
396 for pycache in work_sdist.rglob('**/__pycache__'):
397 shutil.rmtree(pycache)
399 # Ensure derivative sdist is same as original sdist
400 rc = call(['diff', '-qr', orig_sdist, work_sdist], cwd=base)
401 if rc != 0:
402 call(['diff', '-uNr', orig_sdist, work_sdist], cwd=base)
403 raise Exception('Recursive sdist is different')
406 def test_create_git_archive(base, repo):
407 log_test('test_create_git_archive')
408 archive_tgz = base / 'stgit-archive.tar.gz'
409 check_call(
410 ['git', 'archive', '--prefix', 'stgit-archive/', '-o', archive_tgz, 'HEAD'],
411 cwd=repo,
414 with tarfile.open(archive_tgz) as tf:
415 tf.extractall(base)
417 archive_dir = base / 'stgit-archive'
419 assert archive_dir.is_dir()
421 dist = base / 'dist-archive'
423 check_call(
424 [venv_py_exe(base), 'setup.py', 'sdist', '--dist-dir', dist],
425 cwd=archive_dir,
426 stdout=subprocess.DEVNULL,
428 sdist_paths = list(dist.glob('stgit-*.tar.gz'))
429 assert len(sdist_paths) == 1
430 stgit_tgz = sdist_paths[0]
431 log(f'{stgit_tgz=}')
432 prefix = Path(stgit_tgz.with_suffix('').with_suffix('').name)
433 version = version_from_sdist(stgit_tgz)
434 log(f'{version=}')
436 # Ensure generated files are in sdist tarball
437 with tarfile.open(stgit_tgz) as tf:
438 tf.getmember(str(prefix / 'completion' / 'stg.fish'))
439 tf.getmember(str(prefix / 'completion' / 'stgit.bash'))
440 tf.getmember(str(prefix / 'stgit' / 'commands' / 'cmdlist.py'))
441 _version_tar_info = tf.getmember(str(prefix / 'stgit' / '_version.py'))
442 assert 'def _get_version()' not in _version_tar_info.tobuf().decode()
445 def test_build(base, repo):
446 log_test('test_build')
447 build = base / 'build'
448 check_call(
449 [venv_py_exe(base), 'setup.py', 'build', '--build-base', build],
450 cwd=repo,
451 stdout=subprocess.DEVNULL,
454 # Ensure generated files exist in build tree
455 assert (build / 'lib' / 'stgit').is_dir()
456 assert (build / 'lib' / 'stgit' / 'templates').is_dir()
457 assert (build / 'lib' / 'stgit' / 'templates' / 'patchmail.tmpl').is_file()
458 assert (build / 'lib' / 'stgit' / 'commands' / 'cmdlist.py').is_file()
460 # _version.py file in the built tree should be written with a static version
461 assert (
462 'def _get_version()'
463 not in (build / 'lib' / 'stgit' / '_version.py').read_text()
466 # Ensure that generated artifacts also exist in worktree
467 assert (repo / 'stgit' / 'commands' / 'cmdlist.py').is_file()
468 assert (repo / 'completion' / 'stg.fish').is_file()
469 assert (repo / 'completion' / 'stgit.bash').is_file()
471 # The _version.py in the worktree should never be modified
472 assert 'def _get_version()' in (repo / 'stgit' / '_version.py').read_text()
474 # Ensure no checked-in files have been modified by the build
475 check_call(['git', 'diff', '--quiet'], cwd=repo)
478 def test_bdist_wheel(base, repo):
479 log_test('test_bdist_wheel')
480 dist = base / 'dist-whl'
481 check_call(
482 [venv_py_exe(base), 'setup.py', 'bdist_wheel', '--dist-dir', dist],
483 cwd=repo,
484 stdout=subprocess.DEVNULL,
486 sdist_paths = list(dist.glob('stgit-*.whl'))
487 assert len(sdist_paths) == 1
488 stgit_whl = sdist_paths[0]
489 log(f'{stgit_whl=}')
490 version = version_from_wheel(stgit_whl)
491 log(f'{version=}')
493 # Ensure generated files exist in .whl file
494 with zipfile.ZipFile(stgit_whl, 'r') as zf:
495 zf.getinfo('stgit/commands/cmdlist.py')
496 assert 'def _get_version' not in zf.read('stgit/_version.py').decode()
497 zf.getinfo('stgit/templates/patchmail.tmpl')
500 def test_develop_mode(base, repo, cache):
501 log_test('test_develop_mode')
502 # PEP-518 build isolation means build-time deps are installed even
503 # if they're already installed (i.e. already installed in the
504 # virtual env). Thus --find-links is used with --no-index so that
505 # wheel and setuptools can be installed without going to the network.
506 check_call(
508 venv_py_exe(base),
509 '-m',
510 'pip',
511 'install',
512 '--no-index',
513 '--find-links',
514 cache,
515 '-e',
516 '.',
518 cwd=repo,
521 # Gather versions in various ways and ensure they are all consistent
522 py_c_stgit_version = (
523 check_output(
524 [venv_py_exe(base), '-c', 'import stgit; print(stgit.get_version())'],
525 cwd=base,
527 .decode()
528 .rstrip()
530 py_m_stgit_version = version_from_stg_version(
531 check_output([venv_py_exe(base), '-m', 'stgit', '--version'], cwd=base).decode()
533 assert (
534 py_c_stgit_version == py_m_stgit_version
535 ), f'{py_c_stgit_version=} {py_m_stgit_version=}'
537 venv_stg_version = version_from_stg_version(
538 check_output([venv_stg_exe(base), '--version'], cwd=base).decode()
540 assert (
541 py_c_stgit_version == venv_stg_version
542 ), f'{py_c_stgit_version=} {venv_stg_version=}'
544 log(f'{py_c_stgit_version=}')
545 assert 'dirty' not in py_m_stgit_version
547 # Modify a checked-in file to make worktree dirty
548 dirty_file = repo / 'README.md'
549 with open(dirty_file, 'a') as f:
550 print("DIRTY STUFF", file=f)
552 py_c_dirty_version = (
553 check_output(
554 [venv_py_exe(base), '-c', 'import stgit; print(stgit.get_version())'],
555 cwd=base,
557 .decode()
558 .rstrip()
560 py_m_dirty_version = version_from_stg_version(
561 check_output([venv_py_exe(base), '-m', 'stgit', '--version'], cwd=base).decode()
563 assert (
564 py_c_dirty_version == py_m_dirty_version
565 ), f'{py_c_dirty_version=} {py_m_dirty_version=}'
567 venv_std_dirty_version = version_from_stg_version(
568 check_output([venv_stg_exe(base), '--version'], cwd=base).decode()
570 assert (
571 py_c_dirty_version == venv_std_dirty_version
572 ), f'{py_c_dirty_version=} {venv_std_dirty_version=}'
574 log(f'{py_c_dirty_version=}')
575 assert 'dirty' in py_m_dirty_version
576 assert f'{py_m_stgit_version}.dirty' == py_m_dirty_version
578 # Restore worktree to pristine state
579 check_call(['git', 'checkout', '--', dirty_file], cwd=repo)
580 check_call(['git', 'diff', '--quiet'], cwd=repo)
581 check_call(['git', 'clean', '-qfx'], cwd=repo)
584 def test_uninstall_from_venv(base):
585 log_test('test_uninstall_from_venv')
586 check_call(
587 [venv_py_exe(base), '-m', 'pip', 'uninstall', '--yes', 'stgit'], cwd=base
590 venv_path = base / 'venv'
591 assert not (venv_path / 'bin' / 'stg').exists()
592 site_packages = next((venv_path / 'lib').glob('python3.*')) / 'site-packages'
593 assert site_packages.is_dir()
594 assert not list(site_packages.glob('stgit*'))
595 share_dir = venv_path / 'share' / 'stgit'
596 assert not share_dir.exists() or not list(share_dir.iterdir())
599 def test_running_tests_from_sdist(base):
600 log_test('test_running_tests_from_sdist')
601 work_sdist = next((base / 'work-sdist').iterdir())
602 check_call(['./t0000-init.sh'], cwd=work_sdist / 't')
605 def test_install_from_unpacked_sdist(base, cache):
606 log_test('test_install_from_unpacked_sdist')
607 work_sdist = next((base / 'work-sdist').iterdir())
609 check_call(
611 venv_py_exe(base),
612 '-m',
613 'pip',
614 'install',
615 '--no-index',
616 '--find-links',
617 cache,
618 '.',
620 cwd=work_sdist,
622 version = version_from_stg_version(
623 check_output([venv_py_exe(base), '-m', 'stgit', '--version'], cwd=base).decode()
625 assert work_sdist.name.endswith(version)
628 def test_install_sdist(base, cache):
629 log_test('test_install_sdist')
630 sdist_path = next((base / 'dist').iterdir())
631 check_call(
633 venv_py_exe(base),
634 '-m',
635 'pip',
636 'install',
637 '--no-index',
638 '--find-links',
639 cache,
640 sdist_path,
642 cwd=base,
644 version = version_from_stg_version(
645 check_output([venv_py_exe(base), '-m', 'stgit', '--version'], cwd=base).decode()
647 assert sdist_path.name.split('stgit-', 1)[1].startswith(version)
650 def test_install_wheel(base):
651 log_test('test_install_wheel')
652 whl_path = next((base / 'dist-whl').iterdir())
654 check_call(
655 [venv_py_exe(base), '-m', 'pip', 'install', '--no-index', whl_path],
656 cwd=base,
658 version = version_from_stg_version(
659 check_output([venv_py_exe(base), '-m', 'stgit', '--version'], cwd=base).decode()
661 assert whl_path.name.split('stgit-', 1)[1].startswith(version)
664 if __name__ == '__main__':
665 main()