PageRenderTime 47ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/tests/python/pants_test/pants_run_integration_test.py

https://gitlab.com/Ivy001/pants
Python | 333 lines | 247 code | 35 blank | 51 comment | 32 complexity | cb66d57d00f77a21ecf2d5b90ea6f900 MD5 | raw file
  1. # coding=utf-8
  2. # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
  3. # Licensed under the Apache License, Version 2.0 (see LICENSE).
  4. from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
  5. unicode_literals, with_statement)
  6. import ConfigParser
  7. import os
  8. import subprocess
  9. import unittest
  10. from collections import namedtuple
  11. from contextlib import contextmanager
  12. from operator import eq, ne
  13. from colors import strip_color
  14. from pants.base.build_environment import get_buildroot
  15. from pants.base.build_file import BuildFile
  16. from pants.fs.archive import ZIP
  17. from pants.util.contextutil import environment_as, temporary_dir
  18. from pants.util.dirutil import safe_mkdir, safe_open
  19. from pants_test.testutils.file_test_util import check_symlinks, contains_exact_files
  20. PantsResult = namedtuple(
  21. 'PantsResult',
  22. ['command', 'returncode', 'stdout_data', 'stderr_data', 'workdir'])
  23. def ensure_cached(expected_num_artifacts=None):
  24. """Decorator for asserting cache writes in an integration test.
  25. :param expected_num_artifacts: Expected number of artifacts to be in the task's
  26. cache after running the test. If unspecified, will
  27. assert that the number of artifacts in the cache is
  28. non-zero.
  29. """
  30. def decorator(test_fn):
  31. def wrapper(self, *args, **kwargs):
  32. with temporary_dir() as artifact_cache:
  33. cache_args = '--cache-write-to=["{}"]'.format(artifact_cache)
  34. test_fn(self, *args + (cache_args,), **kwargs)
  35. num_artifacts = 0
  36. for (root, _, files) in os.walk(artifact_cache):
  37. print(root, files)
  38. num_artifacts += len(files)
  39. if expected_num_artifacts is None:
  40. self.assertNotEqual(num_artifacts, 0)
  41. else:
  42. self.assertEqual(num_artifacts, expected_num_artifacts)
  43. return wrapper
  44. return decorator
  45. def ensure_engine(f):
  46. """A decorator for running an integration test with and without the v2 engine enabled via
  47. temporary environment variables."""
  48. def wrapper(self, *args, **kwargs):
  49. for env_var_value in ('false', 'true'):
  50. with environment_as(PANTS_ENABLE_V2_ENGINE=env_var_value):
  51. f(self, *args, **kwargs)
  52. return wrapper
  53. class PantsRunIntegrationTest(unittest.TestCase):
  54. """A base class useful for integration tests for targets in the same repo."""
  55. PANTS_SUCCESS_CODE = 0
  56. PANTS_SCRIPT_NAME = 'pants'
  57. @classmethod
  58. def hermetic(cls):
  59. """Subclasses may override to acknowledge that they are hermetic.
  60. That is, that they should run without reading the real pants.ini.
  61. """
  62. return False
  63. @classmethod
  64. def has_python_version(cls, version):
  65. """Returns true if the current system has the specified version of python.
  66. :param version: A python version string, such as 2.6, 3.
  67. """
  68. try:
  69. subprocess.call(['python%s' % version, '-V'])
  70. return True
  71. except OSError:
  72. return False
  73. def temporary_workdir(self, cleanup=True):
  74. # We can hard-code '.pants.d' here because we know that will always be its value
  75. # in the pantsbuild/pants repo (e.g., that's what we .gitignore in that repo).
  76. # Grabbing the pants_workdir config would require this pants's config object,
  77. # which we don't have a reference to here.
  78. root = os.path.join(get_buildroot(), '.pants.d', 'tmp')
  79. safe_mkdir(root)
  80. return temporary_dir(root_dir=root, cleanup=cleanup, suffix='.pants.d')
  81. def temporary_cachedir(self):
  82. return temporary_dir(suffix='__CACHEDIR')
  83. def temporary_sourcedir(self):
  84. return temporary_dir(root_dir=get_buildroot())
  85. @contextmanager
  86. def source_clone(self, source_dir):
  87. with self.temporary_sourcedir() as clone_dir:
  88. target_spec_dir = os.path.relpath(clone_dir)
  89. for dir_path, dir_names, file_names in os.walk(source_dir):
  90. clone_dir_path = os.path.join(clone_dir, os.path.relpath(dir_path, source_dir))
  91. for dir_name in dir_names:
  92. os.mkdir(os.path.join(clone_dir_path, dir_name))
  93. for file_name in file_names:
  94. with open(os.path.join(dir_path, file_name), 'r') as f:
  95. content = f.read()
  96. if BuildFile._is_buildfile_name(file_name):
  97. content = content.replace(source_dir, target_spec_dir)
  98. with open(os.path.join(clone_dir_path, file_name), 'w') as f:
  99. f.write(content)
  100. yield clone_dir
  101. def run_pants_with_workdir(self, command, workdir, config=None, stdin_data=None, extra_env=None,
  102. **kwargs):
  103. args = [
  104. '--no-pantsrc',
  105. '--pants-workdir={}'.format(workdir),
  106. '--kill-nailguns',
  107. '--print-exception-stacktrace',
  108. ]
  109. if self.hermetic():
  110. args.extend(['--pants-config-files=[]',
  111. # Turn off cache globally. A hermetic integration test shouldn't rely on cache,
  112. # or we have no idea if it's actually testing anything.
  113. '--no-cache-read', '--no-cache-write',
  114. # Turn cache on just for tool bootstrapping, for performance.
  115. '--cache-bootstrap-read', '--cache-bootstrap-write'
  116. ])
  117. if config:
  118. config_data = config.copy()
  119. ini = ConfigParser.ConfigParser(defaults=config_data.pop('DEFAULT', None))
  120. for section, section_config in config_data.items():
  121. ini.add_section(section)
  122. for key, value in section_config.items():
  123. ini.set(section, key, value)
  124. ini_file_name = os.path.join(workdir, 'pants.ini')
  125. with safe_open(ini_file_name, mode='w') as fp:
  126. ini.write(fp)
  127. args.append('--config-override=' + ini_file_name)
  128. pants_script = os.path.join(get_buildroot(), self.PANTS_SCRIPT_NAME)
  129. pants_command = [pants_script] + args + command
  130. if self.hermetic():
  131. env = {}
  132. else:
  133. env = os.environ.copy()
  134. if extra_env:
  135. env.update(extra_env)
  136. proc = subprocess.Popen(pants_command, env=env, stdin=subprocess.PIPE,
  137. stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
  138. (stdout_data, stderr_data) = proc.communicate(stdin_data)
  139. return PantsResult(pants_command, proc.returncode, stdout_data.decode("utf-8"),
  140. stderr_data.decode("utf-8"), workdir)
  141. def run_pants(self, command, config=None, stdin_data=None, extra_env=None, **kwargs):
  142. """Runs pants in a subprocess.
  143. :param list command: A list of command line arguments coming after `./pants`.
  144. :param config: Optional data for a generated ini file. A map of <section-name> ->
  145. map of key -> value. If order in the ini file matters, this should be an OrderedDict.
  146. :param kwargs: Extra keyword args to pass to `subprocess.Popen`.
  147. :returns a tuple (returncode, stdout_data, stderr_data).
  148. """
  149. with self.temporary_workdir() as workdir:
  150. return self.run_pants_with_workdir(command, workdir, config, stdin_data, extra_env, **kwargs)
  151. @contextmanager
  152. def pants_results(self, command, config=None, stdin_data=None, extra_env=None, **kwargs):
  153. """Similar to run_pants in that it runs pants in a subprocess, but yields in order to give
  154. callers a chance to do any necessary validations on the workdir.
  155. :param list command: A list of command line arguments coming after `./pants`.
  156. :param config: Optional data for a generated ini file. A map of <section-name> ->
  157. map of key -> value. If order in the ini file matters, this should be an OrderedDict.
  158. :param kwargs: Extra keyword args to pass to `subprocess.Popen`.
  159. :returns a tuple (returncode, stdout_data, stderr_data).
  160. """
  161. with self.temporary_workdir() as workdir:
  162. yield self.run_pants_with_workdir(command, workdir, config, stdin_data, extra_env, **kwargs)
  163. def bundle_and_run(self, target, bundle_name, bundle_jar_name=None, bundle_options=None,
  164. args=None,
  165. expected_bundle_jar_content=None,
  166. expected_bundle_content=None,
  167. library_jars_are_symlinks=True):
  168. """Creates the bundle with pants, then does java -jar {bundle_name}.jar to execute the bundle.
  169. :param target: target name to compile
  170. :param bundle_name: resulting bundle filename (minus .zip extension)
  171. :param bundle_jar_name: monolithic jar filename (minus .jar extension), if None will be the
  172. same as bundle_name
  173. :param bundle_options: additional options for bundle
  174. :param args: optional arguments to pass to executable
  175. :param expected_bundle_content: verify the bundle zip content
  176. :param expected_bundle_jar_content: verify the bundle jar content
  177. :param library_jars_are_symlinks: verify library jars are symlinks if True, and actual
  178. files if False. Default `True` because we always create symlinks for both external and internal
  179. dependencies, only exception is when shading is used.
  180. :return: stdout as a string on success, raises an Exception on error
  181. """
  182. bundle_jar_name = bundle_jar_name or bundle_name
  183. bundle_options = bundle_options or []
  184. bundle_options = ['bundle.jvm'] + bundle_options + ['--archive=zip', target]
  185. with self.pants_results(bundle_options) as pants_run:
  186. self.assert_success(pants_run)
  187. self.assertTrue(check_symlinks('dist/{bundle_name}-bundle/libs'.format(bundle_name=bundle_name),
  188. library_jars_are_symlinks))
  189. # TODO(John Sirois): We need a zip here to suck in external library classpath elements
  190. # pointed to by symlinks in the run_pants ephemeral tmpdir. Switch run_pants to be a
  191. # contextmanager that yields its results while the tmpdir workdir is still active and change
  192. # this test back to using an un-archived bundle.
  193. with temporary_dir() as workdir:
  194. ZIP.extract('dist/{bundle_name}.zip'.format(bundle_name=bundle_name), workdir)
  195. if expected_bundle_content:
  196. self.assertTrue(contains_exact_files(workdir, expected_bundle_content))
  197. if expected_bundle_jar_content:
  198. with temporary_dir() as check_bundle_jar_dir:
  199. bundle_jar = os.path.join(workdir, '{bundle_jar_name}.jar'
  200. .format(bundle_jar_name=bundle_jar_name))
  201. ZIP.extract(bundle_jar, check_bundle_jar_dir)
  202. self.assertTrue(contains_exact_files(check_bundle_jar_dir, expected_bundle_jar_content))
  203. optional_args = []
  204. if args:
  205. optional_args = args
  206. java_run = subprocess.Popen(['java',
  207. '-jar',
  208. '{bundle_jar_name}.jar'.format(bundle_jar_name=bundle_jar_name)]
  209. + optional_args,
  210. stdout=subprocess.PIPE,
  211. cwd=workdir)
  212. stdout, _ = java_run.communicate()
  213. java_returncode = java_run.returncode
  214. self.assertEquals(java_returncode, 0)
  215. return stdout
  216. def assert_success(self, pants_run, msg=None):
  217. self.assert_result(pants_run, self.PANTS_SUCCESS_CODE, expected=True, msg=msg)
  218. def assert_failure(self, pants_run, msg=None):
  219. self.assert_result(pants_run, self.PANTS_SUCCESS_CODE, expected=False, msg=msg)
  220. def assert_result(self, pants_run, value, expected=True, msg=None):
  221. check, assertion = (eq, self.assertEqual) if expected else (ne, self.assertNotEqual)
  222. if check(pants_run.returncode, value):
  223. return
  224. details = [msg] if msg else []
  225. details.append(' '.join(pants_run.command))
  226. details.append('returncode: {returncode}'.format(returncode=pants_run.returncode))
  227. def indent(content):
  228. return '\n\t'.join(content.splitlines())
  229. if pants_run.stdout_data:
  230. details.append('stdout:\n\t{stdout}'.format(stdout=indent(pants_run.stdout_data)))
  231. if pants_run.stderr_data:
  232. details.append('stderr:\n\t{stderr}'.format(stderr=indent(pants_run.stderr_data)))
  233. error_msg = '\n'.join(details)
  234. assertion(value, pants_run.returncode, error_msg)
  235. def normalize(self, s):
  236. """Removes escape sequences (e.g. colored output) and all whitespace from string s."""
  237. return ''.join(strip_color(s).split())
  238. @contextmanager
  239. def file_renamed(self, prefix, test_name, real_name):
  240. real_path = os.path.join(prefix, real_name)
  241. test_path = os.path.join(prefix, test_name)
  242. try:
  243. os.rename(test_path, real_path)
  244. yield
  245. finally:
  246. os.rename(real_path, test_path)
  247. @contextmanager
  248. def temporary_file_content(self, path, content):
  249. """Temporarily write content to a file for the purpose of an integration test."""
  250. path = os.path.realpath(path)
  251. assert path.startswith(
  252. os.path.realpath(get_buildroot())), 'cannot write paths outside of the buildroot!'
  253. assert not os.path.exists(path), 'refusing to overwrite an existing path!'
  254. with open(path, 'wb') as fh:
  255. fh.write(content)
  256. try:
  257. yield
  258. finally:
  259. os.unlink(path)
  260. def do_command(self, *args, **kwargs):
  261. """Wrapper around run_pants method.
  262. :param args: command line arguments used to run pants
  263. :param kwargs: handles 2 keys
  264. success - indicate whether to expect pants run to succeed or fail.
  265. enable_v2_engine - indicate whether to use v2 engine or not.
  266. :return: a PantsResult object
  267. """
  268. success = kwargs.get('success', True)
  269. enable_v2_engine = kwargs.get('enable_v2_engine', False)
  270. cmd = ['--enable-v2-engine'] if enable_v2_engine else []
  271. cmd.extend(list(args))
  272. pants_run = self.run_pants(cmd)
  273. if success:
  274. self.assert_success(pants_run)
  275. else:
  276. self.assert_failure(pants_run)
  277. return pants_run