Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python test runner uses pytest #6661

Merged
merged 2 commits into from
Nov 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions src/python/pants/backend/python/rules/python_test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

from __future__ import absolute_import, division, print_function, unicode_literals

import os.path
import sys
from builtins import str

from pants.engine.fs import Digest, MergedDirectories, Snapshot, UrlToFetch
from pants.engine.isolated_process import ExecuteProcessRequest, FallibleExecuteProcessResult
from pants.engine.legacy.graph import HydratedTarget
from pants.engine.rules import rule
from pants.engine.selectors import Select
from pants.engine.selectors import Get, Select
from pants.rules.core.core_test_model import Status, TestResult


Expand All @@ -19,14 +23,43 @@ class PyTestResult(TestResult):
pass


# TODO: Support deps
# TODO: Support resources
@rule(PyTestResult, [Select(HydratedTarget)])
def run_python_test(target):
# TODO: Actually run tests (https://github.com/pantsbuild/pants/issues/6003)

if 'fail' in target.address.reference():
noun = 'failed'
status = Status.FAILURE
else:
noun = 'passed'
status = Status.SUCCESS
return PyTestResult(status=status, stdout=str('I am a python test which {}'.format(noun)))

# TODO: Inject versions and digests here through some option, rather than hard-coding it.
pex_snapshot = yield Get(Snapshot, UrlToFetch("https://github.com/pantsbuild/pex/releases/download/v1.5.2/pex27",
Digest('8053a79a5e9c2e6e9ace3999666c9df910d6289555853210c1bbbfa799c3ecda', 1757011)))

# TODO: This should be configurable, both with interpreter constraints, and for remote execution.
python_binary = sys.executable

argv = [
'./{}'.format(pex_snapshot.files[0].path),
'-e', 'pytest:main',
'--python', python_binary,
# TODO: This is non-hermetic because pytest will be resolved on the fly by pex27, where it should be hermetically provided in some way.
# We should probably also specify a specific version.
'pytest',
]

merged_input_files = yield Get(
Digest,
MergedDirectories,
MergedDirectories(directories=(target.adaptor.sources.snapshot.directory_digest, pex_snapshot.directory_digest)),
)

request = ExecuteProcessRequest(
argv=tuple(argv),
input_files=merged_input_files,
description='Run pytest for {}'.format(target.address.reference()),
# TODO: This should not be necessary
env={'PATH': os.path.dirname(python_binary)}
)

result = yield Get(FallibleExecuteProcessResult, ExecuteProcessRequest, request)
# TODO: Do something with stderr?
status = Status.SUCCESS if result.exit_code == 0 else Status.FAILURE

yield PyTestResult(status=status, stdout=str(result.stdout))
6 changes: 5 additions & 1 deletion src/python/pants/source/wrapped_globs.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,12 @@ def __init__(self, rel_root, filespec, snapshot, include_dirs=False):

@memoized_property
def files(self):
return tuple(fast_relpath(path, self.rel_root) for path in self.files_relative_to_buildroot)

@memoized_property
def files_relative_to_buildroot(self):
fds = self._snapshot.path_stats if self._include_dirs else self._snapshot.files
return tuple(fast_relpath(fd.path, self.rel_root) for fd in fds)
return tuple(fd.path for fd in fds)

@property
def files_hash(self):
Expand Down
93 changes: 81 additions & 12 deletions tests/python/pants_test/rules/test_test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@

class TestIntegrationTest(PantsRunIntegrationTest):

# TODO: Modify flags (or pytest) so that output is hermetic and deterministic, and doesn't require fuzzy matching
def assert_fuzzy_string_match(self, got, want):
want_lines = want.split('\n')
got_lines = got.split('\n')
self.assertEqual(len(want_lines), len(got_lines), 'Wrong number of lines comparing:\nWANT:\n{}\nGOT:\n{}'.format(want, got))

for line_number, (want_line, got_line) in enumerate(zip(want_lines, got_lines)):
want_parts = want_line.split('SOME_TEXT')
if len(want_parts) == 1:
self.assertEqual(want_line, got_line, "Line {} wrong".format(line_number))
elif len(want_parts) == 2:
self.assertTrue(got_line.startswith(want_parts[0]), 'Line {} Want "{}" to start with "{}"'.format(line_number, got_line, want_parts[0]))
self.assertTrue(got_line.endswith(want_parts[1]), 'Line {} Want "{}" to end with "{}"'.format(line_number, got_line, want_parts[1]))

def test_passing_python_test(self):
args = [
'--no-v1',
Expand All @@ -18,13 +32,25 @@ def test_passing_python_test(self):
]
pants_run = self.run_pants(args)
self.assert_success(pants_run)
self.assertEqual("""I am a python test which passed

testprojects/tests/python/pants/dummies:passing_target ..... SUCCESS
""", pants_run.stdout_data)
self.assertEqual("", pants_run.stderr_data)
self.assertEqual(pants_run.returncode, 0)

self.assert_fuzzy_string_match(
pants_run.stdout_data,
"""============================= test session starts ==============================
platform SOME_TEXT
rootdir: SOME_TEXT
collected 1 item

testprojects/tests/python/pants/dummies/test_pass.py . [100%]

=========================== 1 passed in SOME_TEXT ===========================

testprojects/tests/python/pants/dummies:passing_target ..... SUCCESS
""",
)

def test_failing_python_test(self):
args = [
'--no-v1',
Expand All @@ -34,12 +60,30 @@ def test_failing_python_test(self):
]
pants_run = self.run_pants(args)
self.assert_failure(pants_run)
self.assertEqual("""I am a python test which failed

testprojects/tests/python/pants/dummies:failing_target ..... FAILURE
""", pants_run.stdout_data)
self.assertEqual("", pants_run.stderr_data)
self.assertEqual(pants_run.returncode, 1)
self.assert_fuzzy_string_match(
pants_run.stdout_data,
"""============================= test session starts ==============================
platform SOME_TEXT
rootdir: SOME_TEXT
collected 1 item

testprojects/tests/python/pants/dummies/test_fail.py F [100%]

=================================== FAILURES ===================================
__________________________________ test_fail ___________________________________

def test_fail():
> assert False
E assert False

testprojects/tests/python/pants/dummies/test_fail.py:2: AssertionError
=========================== 1 failed in SOME_TEXT ===========================

testprojects/tests/python/pants/dummies:failing_target ..... FAILURE
""",
)

def test_mixed_python_tests(self):
args = [
Expand All @@ -51,11 +95,36 @@ def test_mixed_python_tests(self):
]
pants_run = self.run_pants(args)
self.assert_failure(pants_run)
self.assertEqual("""I am a python test which failed
I am a python test which passed
self.assertEqual("", pants_run.stderr_data)
self.assertEqual(pants_run.returncode, 1)
self.assert_fuzzy_string_match(
pants_run.stdout_data,
"""============================= test session starts ==============================
platform SOME_TEXT
rootdir: SOME_TEXT
collected 1 item

testprojects/tests/python/pants/dummies/test_fail.py F [100%]

=================================== FAILURES ===================================
__________________________________ test_fail ___________________________________

def test_fail():
> assert False
E assert False

testprojects/tests/python/pants/dummies/test_fail.py:2: AssertionError
=========================== 1 failed in SOME_TEXT ===========================
============================= test session starts ==============================
platform SOME_TEXT
rootdir: SOME_TEXT
collected 1 item

testprojects/tests/python/pants/dummies/test_pass.py . [100%]

=========================== 1 passed in SOME_TEXT ===========================

testprojects/tests/python/pants/dummies:failing_target ..... FAILURE
testprojects/tests/python/pants/dummies:passing_target ..... SUCCESS
""", pants_run.stdout_data)
self.assertEqual("", pants_run.stderr_data)
self.assertEqual(pants_run.returncode, 1)
""",
)