New python test runners API based on Leonid s TC runners

* See https://confluence.jetbrains.com/display/~link/PyCharm+test+runners+protocol
 * See "universalTests" package
This commit is contained in:
Ilya.Kazakevich
2017-02-01 16:23:01 +03:00
parent dec4bc1bba
commit 6e09980157
62 changed files with 3102 additions and 608 deletions
@@ -215,11 +215,17 @@ public abstract class RunConfigurationProducer<T extends RunConfiguration> {
}
protected RunnerAndConfigurationSettings cloneTemplateConfiguration(@NotNull final ConfigurationContext context) {
final RunConfiguration original = context.getOriginalConfiguration(myConfigurationFactory.getType());
return cloneTemplateConfigurationStatic(context, myConfigurationFactory);
}
@NotNull
protected static RunnerAndConfigurationSettings cloneTemplateConfigurationStatic(@NotNull final ConfigurationContext context,
@NotNull final ConfigurationFactory configurationFactory) {
final RunConfiguration original = context.getOriginalConfiguration(configurationFactory.getType());
if (original != null) {
return RunManager.getInstance(context.getProject()).createConfiguration(original.clone(), myConfigurationFactory);
return RunManager.getInstance(context.getProject()).createConfiguration(original.clone(), configurationFactory);
}
return RunManager.getInstance(context.getProject()).createRunConfiguration("", myConfigurationFactory);
return RunManager.getInstance(context.getProject()).createRunConfiguration("", configurationFactory);
}
@NotNull
@@ -482,6 +482,10 @@ completion.show.intention.icon=true
tabs.alphabetical=false
remote.changelist.management.available=false
# Enables https://confluence.jetbrains.com/display/~link/PyCharm+test+runners+protocol
python.tests.enableUniversalTests=true
python.import.always.ask=false
python.django.timeout=30
python.django.timeout.description=Number of seconds to wait for manage.py list commands
@@ -0,0 +1,13 @@
# coding=utf-8
import nose
import sys
from _jb_runner_tools import jb_start_tests, jb_patch_separator, jb_doc_args
from teamcity.nose_report import TeamcityReport
if __name__ == '__main__':
path, targets, additional_args = jb_start_tests()
sys.argv += [path] if path else jb_patch_separator(targets, fs_glue=".", python_glue=".", fs_to_python_glue=":")
sys.argv += additional_args
jb_doc_args("Nosetest", sys.argv)
nose.main(addplugins=[TeamcityReport()])
@@ -0,0 +1,17 @@
# coding=utf-8
import sys
import pytest
from _jb_runner_tools import jb_start_tests, jb_patch_separator, jb_doc_args
from teamcity import pytest_plugin
if __name__ == '__main__':
path, targets, additional_args = jb_start_tests()
sys.argv += additional_args
joined_targets = jb_patch_separator(targets, fs_glue=":", python_glue="::", fs_to_python_glue=".py::")
# When file is launched in py.test it should be file.py: you can't provide it as bare module
joined_targets = [t + ".py" if ":" not in t else t for t in joined_targets]
sys.argv += [path] if path else joined_targets
jb_doc_args("py.test", sys.argv[1:])
pytest.main(sys.argv[1:], [pytest_plugin])
@@ -0,0 +1,99 @@
# coding=utf-8
"""
Tools to implement runners (https://confluence.jetbrains.com/display/~link/PyCharm+test+runners+protocol)
"""
import argparse
import sys, os
import imp
from teamcity import teamcity_presence_env_var, messages
# Some runners need it to "detect" TC and start protocol
if teamcity_presence_env_var not in os.environ:
os.environ[teamcity_presence_env_var] = "LOCAL"
# Monkeypatching TC to pass location hint
old_started = messages.TeamcityServiceMessages.message
def __jb_message(self, messageName, **properties):
try:
properties["locationHint"] = "python://{0}".format(properties["name"])
except KeyError:
pass
print("\n")
old_started(self, messageName, **properties)
messages.TeamcityServiceMessages.message = __jb_message
# Monkeypatched
def jb_patch_separator(targets, fs_glue, python_glue, fs_to_python_glue):
"""
Targets are always dot separated according to manual.
How ever, some runners may need different separators.
This function splits target to file/symbol parts and glues them using provided glues.
:param targets: list of dot-separated targets
:param fs_glue: how to glue fs parts of target. I.e.: module "eggs" in "spam" package is "spam[fs_glue]eggs"
:param python_glue: how to glue python parts (glue between class and function etc)
:param fs_to_python_glue: between last fs-part and first python part
:return: list of targets with patched separators
"""
if not targets:
return []
def _patch_target(target):
path = None
parts = target.split(".")
for i in range(0, len(parts)):
m = parts[i]
try:
(fil, path, desc) = imp.find_module(m, path)
except ImportError:
fs_part = fs_glue.join(parts[:i])
python_path = python_glue.join(parts[i:])
return fs_part + fs_to_python_glue + python_path if python_path else fs_part
if desc[2] == imp.PKG_DIRECTORY:
# Package
path = imp.load_module(m, fil, path, desc).__path__
return target
return map(_patch_target, targets)
def jb_start_tests():
"""
Parses arguments, starts protocol and returns tuple of arguments
:return: (string with path or None, list of targets or None, list of additional arguments)
"""
# Handle additional args after --
additional_args = []
try:
index = sys.argv.index("--")
additional_args = sys.argv[index + 1:]
del sys.argv[index:]
except ValueError:
pass
parser = argparse.ArgumentParser(description='PyCharm test runner')
parser.add_argument('--path', help='Path to file or folder to run')
parser.add_argument('--target', help='Python target to run', action="append")
namespace = parser.parse_args()
del sys.argv[1:] # Remove all args
messages.TeamcityServiceMessages().message('enteredTheMatrix')
return namespace.path, namespace.target, additional_args
def jb_doc_args(framework_name, args):
"""
Runner encouraged to report its arguments to user with aid of this function
"""
print("Launching {0} with arguments {1}".format(framework_name, " ".join(args)))
@@ -0,0 +1,26 @@
# coding=utf-8
import os
from unittest import main
from _jb_runner_tools import jb_start_tests, jb_doc_args
from teamcity import unittestpy
if __name__ == '__main__':
path, targets, additional_args = jb_start_tests()
args = ["python -m unittest"]
if path:
# Unittest does not support script directly, but it can use "discover" to find all tests in some folder
# filtering by script
additional_args.append("discover")
additional_args.append("-s")
assert os.path.exists(path), "{0}: No such file or directory".format(path)
if os.path.isfile(path):
additional_args += [os.path.dirname(path), "-p", os.path.basename(path)]
else:
additional_args.append(path)
else:
additional_args += targets
args += additional_args
jb_doc_args("unittests", args)
main(argv=args, module=None, testRunner=unittestpy.TeamcityTestRunner())
@@ -0,0 +1,4 @@
Test runners wraps python framework and provides teamcity protocol.
They are are distributed separately.
Copy latest version from https://github.com/JetBrains/teamcity-messages.git/teamcity
See https://confluence.jetbrains.com/display/~link/PyCharm+test+runners+protocol
@@ -0,0 +1,12 @@
# coding=utf-8
import os
__all__ = ['is_running_under_teamcity']
__version__ = "1.21"
teamcity_presence_env_var = "TEAMCITY_VERSION"
def is_running_under_teamcity():
return os.getenv(teamcity_presence_env_var) is not None
+51
View File
@@ -0,0 +1,51 @@
# coding=utf-8
import sys
import traceback
import inspect
_max_reported_output_size = 1 * 1024 * 1024
_reported_output_chunk_size = 50000
def limit_output(data):
return data[:_max_reported_output_size]
def split_output(data):
while len(data) > 0:
if len(data) <= _reported_output_chunk_size:
yield data
data = ''
else:
yield data[:_reported_output_chunk_size]
data = data[_reported_output_chunk_size:]
def is_string(obj):
if sys.version_info >= (3, 0):
return isinstance(obj, str)
else:
return isinstance(obj, basestring)
def get_class_fullname(something):
if inspect.isclass(something):
cls = something
else:
cls = something.__class__
module = cls.__module__
if module is None or module == str.__class__.__module__:
return cls.__name__
return module + '.' + cls.__name__
def convert_error_to_string(err):
try:
exctype, value, tb = err
return ''.join(traceback.format_exception(exctype, value, tb))
except:
tb = traceback.format_exc()
return "*FAILED TO GET TRACEBACK*: " + tb
@@ -0,0 +1,53 @@
try:
from contextlib2 import contextmanager
except:
from contextlib import contextmanager
@contextmanager
def block(messages, name, flowId=None):
messages.blockOpened(name, flowId)
yield
messages.blockClosed(name, flowId)
@contextmanager
def compilation(messages, compiler):
messages.compilationStarted(compiler)
yield
messages.compilationFinished(compiler)
@contextmanager
def testSuite(messages, name):
messages.testSuiteStarted(name)
yield
messages.testSuiteFinished(name)
@contextmanager
def test(messages, testName, captureStandardOutput=None, flowId=None, testDuration=None):
messages.testStarted(testName=testName, captureStandardOutput=captureStandardOutput, flowId=flowId)
yield
messages.testFinished(testName=testName, testDuration=testDuration, flowId=flowId)
@contextmanager
def progress(messages, message):
messages.progressStart(message)
yield
messages.progressFinish(message)
@contextmanager
def serviceMessagesDisabled(messages, flowId=None):
messages.disableServiceMessages(flowId=flowId)
yield
messages.enableServiceMessages(flowId=flowId)
@contextmanager
def serviceMessagesEnabled(messages, flowId=None):
messages.enableServiceMessages(flowId=flowId)
yield
messages.disableServiceMessages(flowId=flowId)
@@ -0,0 +1,8 @@
from __future__ import absolute_import
from django.test.runner import DiscoverRunner
from teamcity.unittestpy import TeamcityTestRunner
class TeamcityDjangoRunner(DiscoverRunner):
def run_suite(self, suite, **kwargs):
return TeamcityTestRunner().run(suite)
@@ -0,0 +1,6 @@
try:
from flake8.formatting import base # noqa
except ImportError:
from teamcity.flake8_v2_plugin import * # noqa
else:
from teamcity.flake8_v3_plugin import * # noqa
@@ -0,0 +1,78 @@
try:
import pycodestyle as pep8
except ImportError:
import pep8
import re
from teamcity.messages import TeamcityServiceMessages
from teamcity import __version__, is_running_under_teamcity
enable_teamcity = is_running_under_teamcity()
class TeamcityReport(pep8.StandardReport):
name = 'teamcity'
version = __version__
options_added = False
@classmethod
def add_options(cls, parser):
if not cls.options_added:
parser.add_option('--teamcity', default=False,
action='callback', callback=cls.set_option_callback,
help="Enable teamcity messages")
cls.options_added = True
@classmethod
def set_option_callback(cls, option, opt, value, parser):
global enable_teamcity
enable_teamcity = True
@classmethod
def parse_options(cls, options):
if not enable_teamcity:
return
options.reporter = TeamcityReport
options.report = TeamcityReport(options)
options.jobs = None # needs to be disabled, flake8 overrides the report if enabled
def get_file_results(self):
self._deferred_print.sort()
messages = TeamcityServiceMessages()
normalized_filename = self.filename.replace("\\", "/")
for line_number, offset, code, text, doc in self._deferred_print:
position = '%(path)s:%(row)d:%(col)d' % {
'path': normalized_filename,
'row': self.line_offset + line_number,
'col': offset + 1,
}
error_message = '%s %s' % (code, text)
test_name = 'pep8: %s: %s' % (position, error_message)
messages.testStarted(test_name)
if line_number > len(self.lines):
line = ''
else:
line = self.lines[line_number - 1]
details = [
line.rstrip(),
re.sub(r'\S', ' ', line[:offset]) + '^',
]
if doc:
details.append(doc.strip())
details = '\n'.join(details)
messages.testFailed(test_name, error_message, details)
messages.testFinished(test_name)
return self.file_errors
@@ -0,0 +1,55 @@
import re
from io import BytesIO
from flake8.formatting import base
from teamcity.messages import TeamcityServiceMessages
from teamcity import __version__, is_running_under_teamcity
class TeamcityReport(base.BaseFormatter):
name = 'teamcity-messages'
version = __version__
options_added = False
@classmethod
def add_options(cls, parser):
if not cls.options_added:
parser.add_option('--teamcity',
default=is_running_under_teamcity(),
help="Force output of JetBrains TeamCity service messages")
parser.add_option('--no-teamcity',
default=False,
help="Disable output of JetBrains TeamCity service messages (even under TeamCity build)")
cls.options_added = True
@classmethod
def parse_options(cls, options):
if not options.no_teamcity:
if options.teamcity or is_running_under_teamcity():
options.format = 'teamcity-messages'
def format(self, error):
normalized_filename = error.filename.replace("\\", "/")
position = '%s:%d:%d' % (
normalized_filename, error.line_number, error.column_number)
error_message = '%s %s' % (error.code, error.text)
test_name = 'pep8: %s: %s' % (position, error_message)
line = error.physical_line
offset = error.column_number
details = [
line.rstrip(),
re.sub(r'\S', ' ', line[:offset]) + '^',
]
details = '\n'.join(details)
bytesio = BytesIO()
messages = TeamcityServiceMessages(output=bytesio)
messages.testStarted(test_name)
messages.testFailed(test_name, error_message, details)
messages.testFinished(test_name)
return bytesio.getvalue().decode('UTF-8')
+189
View File
@@ -0,0 +1,189 @@
# coding=utf-8
import sys
import datetime
if sys.version_info < (3, ):
# Python 2
text_type = unicode # flake8: noqa
else:
# Python 3
text_type = str
_quote = {"'": "|'", "|": "||", "\n": "|n", "\r": "|r", '[': '|[', ']': '|]'}
def escape_value(value):
return "".join(_quote.get(x, x) for x in value)
class TeamcityServiceMessages(object):
def __init__(self, output=sys.stdout, now=datetime.datetime.now, encoding='auto'):
if sys.version_info < (3, ) or not hasattr(output, 'buffer'):
self.output = output
else:
self.output = output.buffer
self.now = now
if encoding and encoding != 'auto':
self.encoding = encoding
elif getattr(output, 'encoding', None) or encoding == 'auto':
# Default encoding to 'utf-8' because it sucks if we fail with a
# `UnicodeEncodeError` simply because LANG didn't get propagated to
# a subprocess or something and sys.stdout.encoding is None
self.encoding = getattr(output, 'encoding', None) or 'utf-8'
else:
self.encoding = None
def encode(self, value):
if self.encoding and isinstance(value, text_type):
value = value.encode(self.encoding)
return value
def decode(self, value):
if self.encoding and not isinstance(value, text_type):
value = value.decode(self.encoding)
return value
if sys.version_info < (3, ):
def escapeValue(self, value):
return escape_value(self.encode(value))
else:
def escapeValue(self, value):
return escape_value(self.decode(value))
def message(self, messageName, **properties):
timestamp = self.now().strftime("%Y-%m-%dT%H:%M:%S.") + "%03d" % (self.now().microsecond / 1000)
message = ("##teamcity[%s timestamp='%s'" % (messageName, timestamp))
for k in sorted(properties.keys()):
value = properties[k]
if value is None:
continue
message += (" %s='%s'" % (k, self.escapeValue(value)))
message += ("]\n")
# Python may buffer it for a long time, flushing helps to see real-time result
self.output.write(self.encode(message))
self.output.flush()
def _single_value_message(self, messageName, value):
message = ("##teamcity[%s '%s']\n" % (messageName, self.escapeValue(value)))
# Python may buffer it for a long time, flushing helps to see real-time result
self.output.write(self.encode(message))
self.output.flush()
def blockOpened(self, name, flowId=None):
self.message('blockOpened', name=name, flowId=flowId)
def blockClosed(self, name, flowId=None):
self.message('blockClosed', name=name, flowId=flowId)
def block(self, name, flowId=None):
import teamcity.context_managers as cm
return cm.block(self, name=name, flowId=flowId)
def compilationStarted(self, compiler):
self.message('compilationStarted', compiler=compiler)
def compilationFinished(self, compiler):
self.message('compilationFinished', compiler=compiler)
def compilation(self, compiler):
import teamcity.context_managers as cm
return cm.compilation(self, compiler=compiler)
def testSuiteStarted(self, suiteName, flowId=None):
self.message('testSuiteStarted', name=suiteName, flowId=flowId)
def testSuiteFinished(self, suiteName, flowId=None):
self.message('testSuiteFinished', name=suiteName, flowId=flowId)
def testSuite(self, name):
import teamcity.context_managers as cm
return cm.testSuite(self, name=name)
def testStarted(self, testName, captureStandardOutput=None, flowId=None):
self.message('testStarted', name=testName, captureStandardOutput=captureStandardOutput, flowId=flowId)
def testFinished(self, testName, testDuration=None, flowId=None):
if testDuration is not None:
duration_ms = testDuration.days * 86400000 + \
testDuration.seconds * 1000 + \
int(testDuration.microseconds / 1000)
self.message('testFinished', name=testName, duration=str(duration_ms), flowId=flowId)
else:
self.message('testFinished', name=testName, flowId=flowId)
def test(self, testName, captureStandardOutput=None, testDuration=None, flowId=None):
import teamcity.context_managers as cm
return cm.test(self, testName=testName, captureStandardOutput=captureStandardOutput, testDuration=testDuration, flowId=flowId)
def testIgnored(self, testName, message='', flowId=None):
self.message('testIgnored', name=testName, message=message, flowId=flowId)
def testFailed(self, testName, message='', details='', flowId=None):
self.message('testFailed', name=testName, message=message, details=details, flowId=flowId)
def testStdOut(self, testName, out, flowId=None):
self.message('testStdOut', name=testName, out=out, flowId=flowId)
def testStdErr(self, testName, out, flowId=None):
self.message('testStdErr', name=testName, out=out, flowId=flowId)
def publishArtifacts(self, path, flowId=None):
self._single_value_message('publishArtifacts', path)
def progressMessage(self, message):
self._single_value_message('progressMessage', message)
def progressStart(self, message):
self._single_value_message('progressStart', message)
def progressFinish(self, message):
self._single_value_message('progressFinish', message)
def progress(self, message):
import teamcity.context_managers as cm
return cm.progress(self, message=message)
def buildProblem(self, description, identity):
self.message('buildProblem', description=description, identity=identity)
def buildStatus(self, status, text):
self.message('buildStatus', status=status, text=text)
def setParameter(self, name, value):
self.message('setParameter', name=name, value=value)
def buildStatisticLinesCovered(self, linesCovered):
self.message('buildStatisticValue', key='CodeCoverageAbsLCovered', value=str(linesCovered))
def buildStatisticTotalLines(self, totalLines):
self.message('buildStatisticValue', key='CodeCoverageAbsLTotal', value=str(totalLines))
def buildStatisticLinesUncovered(self, linesUncovered):
self.message('buildStatisticValue', key='CodeCoverageAbsLUncovered', value=str(linesUncovered))
def enableServiceMessages(self, flowId=None):
self.message('enableServiceMessages', flowId=flowId)
def disableServiceMessages(self, flowId=None):
self.message('disableServiceMessages', flowId=flowId)
def serviceMessagesDisabled(self, flowId=None):
import teamcity.context_managers as cm
return cm.serviceMessagesDisabled(self, flowId=flowId)
def serviceMessagesEnabled(self, flowId=None):
import teamcity.context_managers as cm
return cm.serviceMessagesEnabled(self, flowId=flowId)
def importData(self, typeID, pathToXMLFile):
self.message('importData', type=typeID, path=pathToXMLFile)
def customMessage(self, text, status, errorDetails='', flowId=None):
self.message('message', text=text, status=status, errorDetails=errorDetails, flowId=flowId)
@@ -0,0 +1,185 @@
# coding=utf-8
import os
import sys
import datetime
import inspect
from teamcity import is_running_under_teamcity
from teamcity.common import is_string, split_output, limit_output, get_class_fullname, convert_error_to_string
from teamcity.messages import TeamcityServiceMessages
from nose.exc import SkipTest, DeprecatedTest
CONTEXT_SUITE_FQN = "nose.suite.ContextSuite"
# from nose.util.ln
def _ln(label):
label_len = len(label) + 2
chunk = (70 - label_len) // 2
out = '%s %s %s' % ('-' * chunk, label, '-' * chunk)
pad = 70 - len(out)
if pad > 0:
out = out + ('-' * pad)
return out
_captured_output_start_marker = _ln('>> begin captured stdout <<') + "\n"
_captured_output_end_marker = "\n" + _ln('>> end captured stdout <<')
_real_stdout = sys.stdout
class TeamcityReport(object):
name = 'teamcity-report'
score = 10000
def __init__(self):
super(TeamcityReport, self).__init__()
self.messages = TeamcityServiceMessages(_real_stdout)
self.test_started_datetime_map = {}
self.enabled = False
def get_test_id(self, test):
if is_string(test):
return test
# Handle special "tests"
test_class_name = get_class_fullname(test)
if test_class_name == CONTEXT_SUITE_FQN:
if inspect.ismodule(test.context):
module_name = test.context.__name__
return module_name + "." + test.error_context
elif inspect.isclass(test.context):
class_name = get_class_fullname(test.context)
return class_name + "." + test.error_context
test_id = test.id()
real_test = getattr(test, "test", test)
real_test_class_name = get_class_fullname(real_test)
test_arg = getattr(real_test, "arg", tuple())
if (type(test_arg) is tuple or type(test_arg) is list) and len(test_arg) > 0:
# As written in nose.case.FunctionTestCase#__str__ or nose.case.MethodTestCase#__str__
test_arg_str = "%s" % (test_arg,)
if test_id.endswith(test_arg_str):
# Replace '.' in test args with '_' to preserve test hierarchy on TeamCity
test_id = test_id[:len(test_id) - len(test_arg_str)] + test_arg_str.replace('.', '_')
# Force test_id for doctests
if real_test_class_name != "doctest.DocTestCase" and real_test_class_name != "nose.plugins.doctests.DocTestCase":
desc = test.shortDescription()
if desc and desc != test.id():
return "%s (%s)" % (test_id, desc.replace('.', '_'))
return test_id
def configure(self, options, conf):
self.enabled = is_running_under_teamcity()
def options(self, parser, env=os.environ):
pass
def _get_capture_plugin(self, test):
"""
:type test: nose.case.Test
:rtype: nose.plugins.base.Plugin
"""
for plugin in test.config.plugins.plugins:
if plugin.name == "capture":
return plugin
return None
def _capture_plugin_enabled(self, test):
"""
:type test: nose.case.Test
"""
plugin = self._get_capture_plugin(test)
return plugin is not None and plugin.enabled
def _capture_plugin_buffer(self, test):
"""
:type test: nose.case.Test
"""
plugin = self._get_capture_plugin(test)
if plugin is None:
return None
return getattr(plugin, "buffer", None)
def _captureStandardOutput_value(self, test):
"""
:type test: nose.case.Test
"""
if self._capture_plugin_enabled(test):
return 'false'
else:
return 'true'
def report_fail(self, test, fail_type, err):
# workaround nose bug on python 3
if is_string(err[1]):
err = (err[0], Exception(err[1]), err[2])
test_id = self.get_test_id(test)
details = convert_error_to_string(err)
start_index = details.find(_captured_output_start_marker)
end_index = details.find(_captured_output_end_marker)
if 0 <= start_index < end_index:
# do not log test output twice, see report_finish for actual output handling
details = details[:start_index] + details[end_index + len(_captured_output_end_marker):]
self.messages.testFailed(test_id, message=fail_type, details=details, flowId=test_id)
def report_finish(self, test):
test_id = self.get_test_id(test)
captured_output = getattr(test, "capturedOutput", None)
if captured_output is None and self._capture_plugin_enabled(test):
# nose capture does not fill 'capturedOutput' property on successful tests
captured_output = self._capture_plugin_buffer(test)
if captured_output:
for chunk in split_output(limit_output(captured_output)):
self.messages.testStdOut(test_id, chunk, flowId=test_id)
if test_id in self.test_started_datetime_map:
time_diff = datetime.datetime.now() - self.test_started_datetime_map[test_id]
self.messages.testFinished(test_id, testDuration=time_diff, flowId=test_id)
else:
self.messages.testFinished(test_id, flowId=test_id)
def addError(self, test, err):
test_class_name = get_class_fullname(test)
test_id = self.get_test_id(test)
if issubclass(err[0], SkipTest):
self.messages.testIgnored(test_id, message=("SKIPPED: %s" % str(err[1])), flowId=test_id)
self.report_finish(test)
elif issubclass(err[0], DeprecatedTest):
self.messages.testIgnored(test_id, message="Deprecated", flowId=test_id)
self.report_finish(test)
elif test_class_name == CONTEXT_SUITE_FQN:
self.messages.testStarted(test_id, captureStandardOutput=self._captureStandardOutput_value(test), flowId=test_id)
self.report_fail(test, 'error in ' + test.error_context + ' context', err)
self.messages.testFinished(test_id, flowId=test_id)
else:
self.report_fail(test, 'Error', err)
self.report_finish(test)
def addFailure(self, test, err):
self.report_fail(test, 'Failure', err)
self.report_finish(test)
def startTest(self, test):
test_id = self.get_test_id(test)
self.test_started_datetime_map[test_id] = datetime.datetime.now()
self.messages.testStarted(test_id, captureStandardOutput=self._captureStandardOutput_value(test), flowId=test_id)
def addSuccess(self, test):
self.report_finish(test)
@@ -0,0 +1,309 @@
# coding=utf-8
"""
Aaron Buchanan
Nov. 2012
Plug-in for py.test for reporting to TeamCity server
Report results to TeamCity during test execution for immediate reporting
when using TeamCity.
This should be installed as a py.test plugin and will be automatically enabled by running
tests under TeamCity build.
"""
import os
import sys
import re
import traceback
from datetime import timedelta
from teamcity.messages import TeamcityServiceMessages
from teamcity.common import limit_output, split_output, convert_error_to_string
from teamcity import is_running_under_teamcity
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general")
group._addoption('--teamcity', action="count",
dest="teamcity", default=0, help="force output of JetBrains TeamCity service messages")
group._addoption('--no-teamcity', action="count",
dest="no_teamcity", default=0, help="disable output of JetBrains TeamCity service messages")
def pytest_configure(config):
if config.option.no_teamcity >= 1:
enabled = False
elif config.option.teamcity >= 1:
enabled = True
else:
enabled = is_running_under_teamcity()
if enabled:
output_capture_enabled = getattr(config.option, 'capture', 'fd') != 'no'
coverage_controller = _get_coverage_controller(config)
config._teamcityReporting = EchoTeamCityMessages(output_capture_enabled, coverage_controller)
config.pluginmanager.register(config._teamcityReporting)
def pytest_unconfigure(config):
teamcity_reporting = getattr(config, '_teamcityReporting', None)
if teamcity_reporting:
del config._teamcityReporting
config.pluginmanager.unregister(teamcity_reporting)
def _get_coverage_controller(config):
cov_plugin = config.pluginmanager.getplugin('_cov')
if not cov_plugin:
return None
return cov_plugin.cov_controller
class EchoTeamCityMessages(object):
def __init__(self, output_capture_enabled, coverage_controller):
self.coverage_controller = coverage_controller
self.output_capture_enabled = output_capture_enabled
self.teamcity = TeamcityServiceMessages()
self.test_start_reported_mark = set()
self.max_reported_output_size = 1 * 1024 * 1024
self.reported_output_chunk_size = 50000
def get_id_from_location(self, location):
if type(location) is not tuple or len(location) != 3 or not hasattr(location[2], "startswith"):
return None
def convert_file_to_id(filename):
filename = re.sub(r"\.pyc?$", "", filename)
return filename.replace(os.sep, ".").replace("/", ".")
def add_prefix_to_filename_id(filename_id, prefix):
dot_location = filename_id.rfind('.')
if dot_location <= 0 or dot_location >= len(filename_id) - 1:
return None
return filename_id[:dot_location + 1] + prefix + filename_id[dot_location + 1:]
pylint_prefix = '[pylint] '
if location[2].startswith(pylint_prefix):
id_from_file = convert_file_to_id(location[2][len(pylint_prefix):])
return id_from_file + ".Pylint"
if location[2] == "PEP8-check":
id_from_file = convert_file_to_id(location[0])
return id_from_file + ".PEP8"
return None
def format_test_id(self, nodeid, location):
id_from_location = self.get_id_from_location(location)
if id_from_location is not None:
return id_from_location
test_id = nodeid
if test_id.find("::") < 0:
test_id += "::top_level"
test_id = test_id.replace("::()::", "::")
test_id = re.sub(r"\.pyc?::", r"::", test_id)
test_id = test_id.replace(".", "_").replace(os.sep, ".").replace("/", ".").replace('::', '.')
return test_id
def format_location(self, location):
if type(location) is tuple and len(location) == 3:
return "%s:%s (%s)" % (str(location[0]), str(location[1]), str(location[2]))
return str(location)
def pytest_runtest_logstart(self, nodeid, location):
self.ensure_test_start_reported(self.format_test_id(nodeid, location))
def ensure_test_start_reported(self, test_id):
if test_id not in self.test_start_reported_mark:
if self.output_capture_enabled:
capture_standard_output = "false"
else:
capture_standard_output = "true"
self.teamcity.testStarted(test_id, flowId=test_id, captureStandardOutput=capture_standard_output)
self.test_start_reported_mark.add(test_id)
def report_has_output(self, report):
for (secname, data) in report.sections:
if report.when in secname and ('stdout' in secname or 'stderr' in secname):
return True
return False
def report_test_output(self, report, test_id):
for (secname, data) in report.sections:
# https://github.com/JetBrains/teamcity-messages/issues/112
# CollectReport doesn't have 'when' property
if hasattr(report, "when") and report.when not in secname:
continue
if not data:
continue
if 'stdout' in secname:
for chunk in split_output(limit_output(data)):
self.teamcity.testStdOut(test_id, out=chunk, flowId=test_id)
elif 'stderr' in secname:
for chunk in split_output(limit_output(data)):
self.teamcity.testStdErr(test_id, out=chunk, flowId=test_id)
def report_test_finished(self, test_id, duration=None):
self.teamcity.testFinished(test_id, testDuration=duration, flowId=test_id)
self.test_start_reported_mark.remove(test_id)
def report_test_failure(self, test_id, report, message=None, report_output=True):
if hasattr(report, 'duration'):
duration = timedelta(seconds=report.duration)
else:
duration = None
if message is None:
message = self.format_location(report.location)
self.ensure_test_start_reported(test_id)
if report_output:
self.report_test_output(report, test_id)
self.teamcity.testFailed(test_id, message, str(report.longrepr), flowId=test_id)
self.report_test_finished(test_id, duration)
def report_test_skip(self, test_id, report):
if type(report.longrepr) is tuple and len(report.longrepr) == 3:
reason = report.longrepr[2]
else:
reason = str(report.longrepr)
if hasattr(report, 'duration'):
duration = timedelta(seconds=report.duration)
else:
duration = None
self.ensure_test_start_reported(test_id)
self.report_test_output(report, test_id)
self.teamcity.testIgnored(test_id, reason, flowId=test_id)
self.report_test_finished(test_id, duration)
def pytest_runtest_logreport(self, report):
"""
:type report: _pytest.runner.TestReport
"""
test_id = self.format_test_id(report.nodeid, report.location)
duration = timedelta(seconds=report.duration)
if report.passed:
# Do not report passed setup/teardown if no output
if report.when == 'call':
self.ensure_test_start_reported(test_id)
self.report_test_output(report, test_id)
self.report_test_finished(test_id, duration)
else:
if self.report_has_output(report):
block_name = "test " + report.when
self.teamcity.blockOpened(block_name, flowId=test_id)
self.report_test_output(report, test_id)
self.teamcity.blockClosed(block_name, flowId=test_id)
elif report.failed:
if report.when == 'call':
self.report_test_failure(test_id, report)
elif report.when == 'setup':
if self.report_has_output(report):
self.teamcity.blockOpened("test setup", flowId=test_id)
self.report_test_output(report, test_id)
self.teamcity.blockClosed("test setup", flowId=test_id)
self.report_test_failure(test_id, report, message="test setup failed", report_output=False)
elif report.when == 'teardown':
# Report failed teardown as a separate test as original test is already finished
self.report_test_failure(test_id + "_teardown", report)
elif report.skipped:
self.report_test_skip(test_id, report)
def pytest_collectreport(self, report):
test_id = self.format_test_id(report.nodeid, report.location) + "_collect"
if report.failed:
self.report_test_failure(test_id, report)
elif report.skipped:
self.report_test_skip(test_id, report)
def pytest_terminal_summary(self):
if self.coverage_controller is not None:
try:
self._report_coverage()
except:
tb = traceback.format_exc()
self.teamcity.customMessage("Coverage statistics reporting failed", "ERROR", errorDetails=tb)
def _report_coverage(self):
from coverage.misc import NotPython
from coverage.report import Reporter
from coverage.results import Numbers
class _CoverageReporter(Reporter):
def __init__(self, coverage, config, messages):
super(_CoverageReporter, self).__init__(coverage, config)
self.branches = coverage.data.has_arcs()
self.messages = messages
def report(self, morfs, outfile=None):
if hasattr(self, 'find_code_units'):
self.find_code_units(morfs)
else:
self.find_file_reporters(morfs)
total = Numbers()
if hasattr(self, 'code_units'):
units = self.code_units
else:
units = self.file_reporters
for cu in units:
try:
analysis = self.coverage._analyze(cu)
nums = analysis.numbers
total += nums
except KeyboardInterrupt:
raise
except:
if self.config.ignore_errors:
continue
err = sys.exc_info()
typ, msg = err[:2]
if typ is NotPython and not cu.should_be_python():
continue
test_id = cu.name
details = convert_error_to_string(err)
self.messages.testStarted(test_id, flowId=test_id)
self.messages.testFailed(test_id, message="Coverage analysis failed", details=details, flowId=test_id)
self.messages.testFinished(test_id, flowId=test_id)
if total.n_files > 0:
covered = total.n_executed
total_statements = total.n_statements
if self.branches:
covered += total.n_executed_branches
total_statements += total.n_branches
self.messages.buildStatisticLinesCovered(covered)
self.messages.buildStatisticTotalLines(total_statements)
self.messages.buildStatisticLinesUncovered(total_statements - covered)
reporter = _CoverageReporter(
self.coverage_controller.cov,
self.coverage_controller.cov.config,
self.teamcity,
)
reporter.report(None)
@@ -0,0 +1,171 @@
# coding=utf-8
import sys
from unittest import TestResult, TextTestRunner
import datetime
import re
from teamcity.messages import TeamcityServiceMessages
from teamcity.common import is_string, get_class_fullname, convert_error_to_string
_real_stdout = sys.stdout
class TeamcityTestResult(TestResult):
separator2 = "\n"
def __init__(self, stream=_real_stdout, descriptions=None, verbosity=None):
super(TeamcityTestResult, self).__init__()
self.test_started_datetime_map = {}
self.failed_tests = set()
self.subtest_failures = {}
self.messages = TeamcityServiceMessages(stream)
def get_test_id(self, test):
if is_string(test):
return test
# Force test_id for doctests
if get_class_fullname(test) != "doctest.DocTestCase":
desc = test.shortDescription()
test_method_name = getattr(test, "_testMethodName", "")
if desc and desc != test.id() and desc != test_method_name:
return "%s (%s)" % (test.id(), desc.replace('.', '_'))
return test.id()
def addSuccess(self, test):
super(TeamcityTestResult, self).addSuccess(test)
def addExpectedFailure(self, test, err):
super(TeamcityTestResult, self).addExpectedFailure(test, err)
err = convert_error_to_string(err)
test_id = self.get_test_id(test)
self.messages.testIgnored(test_id, message="Expected failure: " + err, flowId=test_id)
def addSkip(self, test, reason=""):
if sys.version_info >= (2, 7):
super(TeamcityTestResult, self).addSkip(test, reason)
test_id = self.get_test_id(test)
if reason:
reason_str = ": " + str(reason)
else:
reason_str = ""
self.messages.testIgnored(test_id, message="Skipped" + reason_str, flowId=test_id)
def addUnexpectedSuccess(self, test):
super(TeamcityTestResult, self).addUnexpectedSuccess(test)
test_id = self.get_test_id(test)
self.messages.testFailed(test_id, message='Failure',
details="Test should not succeed since it's marked with @unittest.expectedFailure",
flowId=test_id)
def addError(self, test, err, *k):
super(TeamcityTestResult, self).addError(test, err)
if get_class_fullname(test) == "unittest.suite._ErrorHolder":
# This is a standalone error
test_name = test.id()
# patch setUpModule (__main__) -> __main__.setUpModule
test_name = re.sub(r'^(.*) \((.*)\)$', r'\2.\1', test_name)
self.messages.testStarted(test_name, flowId=test_name)
self.report_fail(test_name, 'Failure', err)
self.messages.testFinished(test_name, flowId=test_name)
elif get_class_fullname(err[0]) == "unittest2.case.SkipTest":
message = ""
if hasattr(err[1], "message"):
message = getattr(err[1], "message", "")
elif hasattr(err[1], "args"):
message = getattr(err[1], "args", [""])[0]
self.addSkip(test, message)
else:
self.report_fail(test, 'Error', err)
def addFailure(self, test, err, *k):
super(TeamcityTestResult, self).addFailure(test, err)
self.report_fail(test, 'Failure', err)
def addSubTest(self, test, subtest, err):
super(TeamcityTestResult, self).addSubTest(test, subtest, err)
test_id = self.get_test_id(test)
if err is not None:
if issubclass(err[0], test.failureException):
self.add_subtest_failure(test_id, self.get_test_id(subtest), err)
self.messages.testStdErr(test_id, out="%s: failure\n" % self.get_test_id(subtest), flowId=test_id)
else:
self.add_subtest_failure(test_id, self.get_test_id(subtest), err)
self.messages.testStdErr(test_id, out="%s: error\n" % self.get_test_id(subtest), flowId=test_id)
else:
self.messages.testStdOut(test_id, out="%s: ok\n" % self.get_test_id(subtest), flowId=test_id)
def add_subtest_failure(self, test_id, subtest_id, err):
fail_array = self.subtest_failures.get(test_id, [])
fail_array.append("%s:\n%s" % (subtest_id, convert_error_to_string(err)))
self.subtest_failures[test_id] = fail_array
def get_subtest_failure(self, test_id):
fail_array = self.subtest_failures.get(test_id, [])
return "\n".join(fail_array)
def report_fail(self, test, fail_type, err):
test_id = self.get_test_id(test)
if is_string(err):
details = err
elif get_class_fullname(err) == "twisted.python.failure.Failure":
details = err.getTraceback()
else:
details = convert_error_to_string(err)
subtest_failure = self.get_subtest_failure(test_id)
if subtest_failure:
details = subtest_failure + "\n" + details
self.messages.testFailed(test_id, message=fail_type, details=details, flowId=test_id)
self.failed_tests.add(test_id)
def startTest(self, test):
super(TeamcityTestResult, self).startTest(test)
test_id = self.get_test_id(test)
self.test_started_datetime_map[test_id] = datetime.datetime.now()
self.messages.testStarted(test_id, captureStandardOutput='true', flowId=test_id)
def stopTest(self, test):
super(TeamcityTestResult, self).stopTest(test)
test_id = self.get_test_id(test)
if test_id not in self.failed_tests and self.subtest_failures.get(test_id, []):
self.report_fail(test, "Subtest failed", "")
time_diff = datetime.datetime.now() - self.test_started_datetime_map[test_id]
self.messages.testFinished(test_id, testDuration=time_diff, flowId=test_id)
def printErrors(self):
pass
class TeamcityTestRunner(TextTestRunner):
resultclass = TeamcityTestResult
if sys.version_info < (2, 7):
def _makeResult(self):
return TeamcityTestResult(self.stream, self.descriptions, self.verbosity)
if __name__ == '__main__':
from unittest import main
main(module=None, testRunner=TeamcityTestRunner())
@@ -37,6 +37,7 @@ public abstract class PyPsiFacade {
return ServiceManager.getService(project, PyPsiFacade.class);
}
@NotNull
public abstract QualifiedNameResolver qualifiedNameResolver(String qNameString);
public abstract QualifiedNameResolver qualifiedNameResolver(QualifiedName qualifiedName);
+3 -1
View File
@@ -151,7 +151,8 @@
<xdebugger.settings implementation="com.jetbrains.python.debugger.settings.PyDebuggerSettings"/>
<xdebugger.localAttachDebuggerProvider implementation="com.jetbrains.python.debugger.attach.PyLocalAttachDebuggerProvider"/>
<configurationType implementation="com.jetbrains.python.testing.PythonTestConfigurationType"/>
<configurationType implementation="com.jetbrains.python.testing.PythonTestOldConfigurationType"/>
<configurationType implementation="com.jetbrains.python.testing.universalTests.PyUniversalTestsConfigurationType"/>
<configurationType implementation="com.jetbrains.python.testing.tox.PyToxConfigurationType"/>
<runConfigurationProducer implementation="com.jetbrains.python.testing.unittest.PythonUnitTestConfigurationProducer"/>
@@ -159,6 +160,7 @@
<runConfigurationProducer implementation="com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer"/>
<runConfigurationProducer implementation="com.jetbrains.python.testing.tox.PyToxConfigurationProducer"/>
<runConfigurationProducer implementation="com.jetbrains.python.testing.nosetest.PythonNoseTestConfigurationProducer"/>
<runConfigurationProducer implementation="com.jetbrains.python.testing.universalTests.PyUniversalTestsConfigurationProducer"/>
<codeInsight.lineMarkerProvider language="Python" implementationClass="com.jetbrains.python.codeInsight.PyLineMarkerProvider"/>
<runLineMarkerContributor language="Python" implementationClass="com.jetbrains.python.codeInsight.PyRunLineMarkerContributor"/>
@@ -0,0 +1,33 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.vfs.VirtualFile;
/**
* Descriptor to choose only .py files and (probably) folders.
* @author Ilya.Kazakevich
*/
public final class PyFileChooserDescriptor extends FileChooserDescriptor {
public PyFileChooserDescriptor(final boolean chooseFolders) {super(true, chooseFolders, false, false, false, false);}
@Override
public boolean isFileVisible(final VirtualFile file, final boolean showHiddenFiles) {
return file.isDirectory() || file.getExtension() == null || Comparing.equal(file.getExtension(), "py");
}
}
@@ -56,6 +56,12 @@ public enum PythonHelper implements HelperPackage {
PYTEST("pycharm", "pytestrunner"),
DOCSTRING("pycharm", "docrunner"),
// Runners for new test runner API.
UNIVERSAL_UT("pycharm", "_jb_unittest_runner"),
UNIVERSAL_PYTEST("pycharm", "_jb_pytest_runner"),
UNIVERSAL_NOSE("pycharm", "_jb_nosetest_runner"),
BEHAVE("pycharm", "behave_runner"),
LETTUCE("pycharm", "lettuce_runner"),
@@ -44,6 +44,7 @@ public class PyPsiFacadeImpl extends PyPsiFacade {
}
@Override
@NotNull
public QualifiedNameResolver qualifiedNameResolver(String qNameString) {
return new QualifiedNameResolverImpl(qNameString);
}
@@ -18,11 +18,9 @@ package com.jetbrains.python.run;
import com.google.common.collect.Lists;
import com.intellij.diagnostic.logging.LogConfigurationPanel;
import com.intellij.execution.ExecutionBundle;
import com.intellij.execution.Location;
import com.intellij.execution.configuration.AbstractRunConfiguration;
import com.intellij.execution.configuration.EnvironmentVariablesComponent;
import com.intellij.execution.configurations.*;
import com.intellij.execution.testframework.AbstractTestProxy;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleType;
@@ -37,17 +35,12 @@ import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.PathMappingSettings;
import com.intellij.util.PlatformUtils;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PythonModuleTypeBase;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.testing.PyPsiLocationWithFixedClass;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -61,12 +54,7 @@ import java.util.Map;
* @author Leonid Shalupov
*/
public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRunConfiguration<T>> extends AbstractRunConfiguration
implements LocatableConfiguration, AbstractPythonRunConfigurationParams, CommandLinePatcher {
/**
* When passing path to test to runners, you should join parts with this char.
* I.e.: file.py::PyClassTest::test_method
*/
public static final String TEST_NAME_PARTS_SPLITTER = "::";
implements AbstractPythonRunConfigurationParams, CommandLinePatcher {
private String myInterpreterOptions = "";
private String myWorkingDirectory = "";
private String mySdkHome = "";
@@ -231,7 +219,7 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
}
}
public void readExternal(Element element) throws InvalidDataException {
public void readExternal(@NotNull Element element) throws InvalidDataException {
super.readExternal(element);
myInterpreterOptions = JDOMExternalizerUtil.readField(element, "INTERPRETER_OPTIONS");
readEnvs(element);
@@ -259,7 +247,7 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
EnvironmentVariablesComponent.readExternal(element, getEnvs());
}
public void writeExternal(Element element) throws WriteExternalException {
public void writeExternal(@NotNull Element element) throws WriteExternalException {
super.writeExternal(element);
JDOMExternalizerUtil.writeField(element, "INTERPRETER_OPTIONS", myInterpreterOptions);
writeEnvs(element);
@@ -424,33 +412,6 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractPythonRun
return true;
}
/**
* Create test spec (string to be passed to runner, probably glued with {@link #TEST_NAME_PARTS_SPLITTER})
* @param location test location as reported by runner
* @param failedTest failed test
* @return string spec or null if spec calculation is impossible
*/
@Nullable
public String getTestSpec(@NotNull final Location<?> location, @NotNull final AbstractTestProxy failedTest) {
PsiElement element = location.getPsiElement();
PyClass pyClass = PsiTreeUtil.getParentOfType(element, PyClass.class, false);
if (location instanceof PyPsiLocationWithFixedClass) {
pyClass = ((PyPsiLocationWithFixedClass)location).getFixedClass();
}
PyFunction pyFunction = PsiTreeUtil.getParentOfType(element, PyFunction.class, false);
final VirtualFile virtualFile = location.getVirtualFile();
if (virtualFile != null) {
String path = virtualFile.getCanonicalPath();
if (pyClass != null) {
path += TEST_NAME_PARTS_SPLITTER + pyClass.getName();
}
if (pyFunction != null) {
path += TEST_NAME_PARTS_SPLITTER + pyFunction.getName();
}
return path;
}
return null;
}
/**
* Note to inheritors: Always check {@link #getWorkingDirectory()} first. You should return it, if it is not empty since
@@ -28,6 +28,7 @@ import com.intellij.ui.PanelWithAnchor;
import com.intellij.ui.RawCommandLineEditor;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.jetbrains.python.PyFileChooserDescriptor;
import com.jetbrains.python.debugger.PyDebuggerOptionsProvider;
import org.jetbrains.annotations.NotNull;
@@ -57,12 +58,7 @@ public class PythonRunConfigurationForm implements PythonRunConfigurationParams,
myProject = configuration.getProject();
FileChooserDescriptor chooserDescriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
@Override
public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
return file.isDirectory() || file.getExtension() == null || Comparing.equal(file.getExtension(), "py");
}
};
FileChooserDescriptor chooserDescriptor = new PyFileChooserDescriptor(false);
//chooserDescriptor.setRoot(s.getProject().getBaseDir());
ComponentWithBrowseButton.BrowseFolderActionListener<JTextField> listener =
@@ -0,0 +1,392 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.RefactoringListenerProvider;
import com.intellij.execution.configurations.RuntimeConfigurationError;
import com.intellij.execution.configurations.RuntimeConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.listeners.RefactoringElementAdapter;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
import com.jetbrains.python.run.AbstractPythonRunConfigurationParams;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
/**
* Parent of all python test old-style test runners.
* For new style see {@link com.jetbrains.python.testing.universalTests}
* User: catherine
*/
public abstract class AbstractPythonOldTestRunConfiguration<T extends AbstractPythonTestRunConfiguration<T>>
extends AbstractPythonTestRunConfiguration<T>
implements AbstractPythonRunConfigurationParams,
AbstractPythonTestRunConfigurationParams,
RefactoringListenerProvider {
protected String myClassName = "";
protected String myScriptName = "";
protected String myMethodName = "";
protected String myFolderName = "";
protected TestType myTestType = TestType.TEST_SCRIPT;
private String myPattern = ""; // pattern for modules in folder to match against
private boolean usePattern = false;
protected AbstractPythonOldTestRunConfiguration(Project project, ConfigurationFactory configurationFactory) {
super(project, configurationFactory);
}
@NotNull
@Override
public String getWorkingDirectorySafe() {
final String workingDirectoryFromConfig = getWorkingDirectory();
if (StringUtil.isNotEmpty(workingDirectoryFromConfig)) {
return workingDirectoryFromConfig;
}
final String folderName = myFolderName;
if (!StringUtil.isEmptyOrSpaces(folderName)) {
return folderName;
}
final String scriptName = myScriptName;
if (!StringUtil.isEmptyOrSpaces(scriptName)) {
final VirtualFile script = LocalFileSystem.getInstance().findFileByPath(scriptName);
if (script != null) {
return script.getParent().getPath();
}
}
return super.getWorkingDirectorySafe();
}
@Override
public void readExternal(Element element) throws InvalidDataException {
super.readExternal(element);
myScriptName = JDOMExternalizerUtil.readField(element, "SCRIPT_NAME");
myClassName = JDOMExternalizerUtil.readField(element, "CLASS_NAME");
myMethodName = JDOMExternalizerUtil.readField(element, "METHOD_NAME");
myFolderName = JDOMExternalizerUtil.readField(element, "FOLDER_NAME");
myPattern = JDOMExternalizerUtil.readField(element, "PATTERN");
usePattern = Boolean.parseBoolean(JDOMExternalizerUtil.readField(element, "USE_PATTERN"));
try {
final String testType = JDOMExternalizerUtil.readField(element, "TEST_TYPE");
myTestType = testType != null ? TestType.valueOf(testType) : TestType.TEST_SCRIPT;
}
catch (IllegalArgumentException e) {
myTestType = TestType.TEST_SCRIPT; // safe default
}
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
super.writeExternal(element);
JDOMExternalizerUtil.writeField(element, "SCRIPT_NAME", myScriptName);
JDOMExternalizerUtil.writeField(element, "CLASS_NAME", myClassName);
JDOMExternalizerUtil.writeField(element, "METHOD_NAME", myMethodName);
JDOMExternalizerUtil.writeField(element, "FOLDER_NAME", myFolderName);
JDOMExternalizerUtil.writeField(element, "TEST_TYPE", myTestType.toString());
JDOMExternalizerUtil.writeField(element, "PATTERN", myPattern);
JDOMExternalizerUtil.writeField(element, "USE_PATTERN", String.valueOf(usePattern));
}
public AbstractPythonRunConfigurationParams getBaseParams() {
return this;
}
public String getClassName() {
return myClassName;
}
public void setClassName(String className) {
myClassName = className;
}
public String getFolderName() {
return myFolderName;
}
public void setFolderName(String folderName) {
myFolderName = folderName;
}
public String getScriptName() {
return myScriptName;
}
public void setScriptName(String scriptName) {
myScriptName = scriptName;
}
public String getMethodName() {
return myMethodName;
}
public void setMethodName(String methodName) {
myMethodName = methodName;
}
public TestType getTestType() {
return myTestType;
}
public void setTestType(TestType testType) {
myTestType = testType;
}
public String getPattern() {
return myPattern;
}
public void setPattern(String pattern) {
myPattern = pattern;
}
public boolean usePattern() {
return usePattern;
}
public void usePattern(boolean usePattern) {
this.usePattern = usePattern;
}
public enum TestType {
TEST_FOLDER,
TEST_SCRIPT,
TEST_CLASS,
TEST_METHOD,
TEST_FUNCTION,
}
@Override
public void checkConfiguration() throws RuntimeConfigurationException {
super.checkConfiguration();
if (StringUtil.isEmptyOrSpaces(myFolderName) && myTestType == TestType.TEST_FOLDER) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_folder_name"));
}
if (StringUtil.isEmptyOrSpaces(getScriptName()) && myTestType != TestType.TEST_FOLDER) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_script_name"));
}
if (StringUtil.isEmptyOrSpaces(myClassName) && (myTestType == TestType.TEST_METHOD || myTestType == TestType.TEST_CLASS)) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_class_name"));
}
if (StringUtil.isEmptyOrSpaces(myMethodName) && (myTestType == TestType.TEST_METHOD || myTestType == TestType.TEST_FUNCTION)) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_method_name"));
}
}
public boolean compareSettings(AbstractPythonOldTestRunConfiguration cfg) {
if (cfg == null) return false;
if (getTestType() != cfg.getTestType()) return false;
switch (getTestType()) {
case TEST_FOLDER:
return getFolderName().equals(cfg.getFolderName());
case TEST_SCRIPT:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory());
case TEST_CLASS:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory()) &&
getClassName().equals(cfg.getClassName());
case TEST_METHOD:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory()) &&
getClassName().equals(cfg.getClassName()) &&
getMethodName().equals(cfg.getMethodName());
case TEST_FUNCTION:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory()) &&
getMethodName().equals(cfg.getMethodName());
default:
throw new IllegalStateException("Unknown test type: " + getTestType());
}
}
public static void copyParams(AbstractPythonTestRunConfigurationParams source, AbstractPythonTestRunConfigurationParams target) {
AbstractPythonRunConfiguration.copyParams(source.getBaseParams(), target.getBaseParams());
target.setScriptName(source.getScriptName());
target.setClassName(source.getClassName());
target.setFolderName(source.getFolderName());
target.setMethodName(source.getMethodName());
target.setTestType(source.getTestType());
target.setPattern(source.getPattern());
target.usePattern(source.usePattern());
target.setAddContentRoots(source.shouldAddContentRoots());
target.setAddSourceRoots(source.shouldAddSourceRoots());
}
public AbstractPythonTestRunConfigurationParams getTestRunConfigurationParams() {
return this;
}
@Override
public String suggestedName() {
switch (myTestType) {
case TEST_CLASS:
return getPluralTitle() + " in " + myClassName;
case TEST_METHOD:
return getTitle() + " " + myClassName + "." + myMethodName;
case TEST_SCRIPT:
String name = new File(getScriptName()).getName();
name = StringUtil.trimEnd(name, ".py");
return getPluralTitle() + " in " + name;
case TEST_FOLDER:
String folderName = new File(myFolderName).getName();
return getPluralTitle() + " in " + folderName;
case TEST_FUNCTION:
return getTitle() + " " + myMethodName;
default:
throw new IllegalStateException("Unknown test type: " + myTestType);
}
}
@Nullable
@Override
public String getActionName() {
if (TestType.TEST_METHOD.equals(myTestType)) {
return getTitle() + " " + myMethodName;
}
return suggestedName();
}
protected abstract String getTitle();
protected abstract String getPluralTitle();
@Override
public RefactoringElementListener getRefactoringElementListener(PsiElement element) {
if (element instanceof PsiDirectory) {
VirtualFile vFile = ((PsiDirectory)element).getVirtualFile();
if ((myTestType == TestType.TEST_FOLDER && pathsEqual(vFile, myFolderName)) || pathsEqual(vFile, getWorkingDirectory())) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
String newPath = FileUtil.toSystemDependentName(((PsiDirectory)newElement).getVirtualFile().getPath());
setWorkingDirectory(newPath);
if (myTestType == TestType.TEST_FOLDER) {
myFolderName = newPath;
}
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
final String systemDependant = FileUtil.toSystemDependentName(oldQualifiedName);
setWorkingDirectory(systemDependant);
if (myTestType == TestType.TEST_FOLDER) {
myFolderName = systemDependant;
}
}
};
}
return null;
}
if (myTestType == TestType.TEST_FOLDER) {
return null;
}
File scriptFile = new File(myScriptName);
if (!scriptFile.isAbsolute()) {
scriptFile = new File(getWorkingDirectory(), myScriptName);
}
PsiFile containingFile = element.getContainingFile();
VirtualFile vFile = containingFile == null ? null : containingFile.getVirtualFile();
if (vFile != null && Comparing.equal(new File(vFile.getPath()).getAbsolutePath(), scriptFile.getAbsolutePath())) {
if (element instanceof PsiFile) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
VirtualFile virtualFile = ((PsiFile)newElement).getVirtualFile();
if (virtualFile != null) {
myScriptName = FileUtil.toSystemDependentName(virtualFile.getPath());
}
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
myScriptName = FileUtil.toSystemDependentName(oldQualifiedName);
}
};
}
if (element instanceof PyClass && (myTestType == TestType.TEST_CLASS || myTestType == TestType.TEST_METHOD) &&
Comparing.equal(((PyClass)element).getName(), myClassName)) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
myClassName = ((PyClass)newElement).getName();
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
myClassName = oldQualifiedName;
}
};
}
if (element instanceof PyFunction &&
Comparing.equal(((PyFunction)element).getName(), myMethodName)) {
ScopeOwner scopeOwner = PsiTreeUtil.getParentOfType(element, ScopeOwner.class);
if ((myTestType == TestType.TEST_FUNCTION && scopeOwner instanceof PyFile) ||
(myTestType == TestType.TEST_METHOD && scopeOwner instanceof PyClass && Comparing.equal(scopeOwner.getName(), myClassName))) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
myMethodName = ((PyFunction)newElement).getName();
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
final int methodIdx = oldQualifiedName.indexOf("#") + 1;
if (methodIdx > 0 && methodIdx < oldQualifiedName.length()) {
myMethodName = oldQualifiedName.substring(methodIdx);
}
}
};
}
}
}
return null;
}
private static boolean pathsEqual(VirtualFile vFile, final String folderName) {
return Comparing.equal(new File(vFile.getPath()).getAbsolutePath(), new File(folderName).getAbsolutePath());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,372 +15,62 @@
*/
package com.jetbrains.python.testing;
import com.intellij.execution.Location;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.RefactoringListenerProvider;
import com.intellij.execution.configurations.RuntimeConfigurationError;
import com.intellij.execution.configurations.RuntimeConfigurationException;
import com.intellij.execution.testframework.AbstractTestProxy;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.listeners.RefactoringElementAdapter;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
import com.jetbrains.python.run.AbstractPythonRunConfigurationParams;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
/**
* User: catherine
* Parent of all test configurations
*
* @author Ilya.Kazakevich
*/
public abstract class AbstractPythonTestRunConfiguration<T extends AbstractPythonRunConfiguration<T>> extends AbstractPythonRunConfiguration<T>
implements AbstractPythonRunConfigurationParams,
AbstractPythonTestRunConfigurationParams,
RefactoringListenerProvider {
protected String myClassName = "";
protected String myScriptName = "";
protected String myMethodName = "";
protected String myFolderName = "";
protected TestType myTestType = TestType.TEST_SCRIPT;
public abstract class AbstractPythonTestRunConfiguration<T extends AbstractPythonTestRunConfiguration<T>>
extends AbstractPythonRunConfiguration<T> {
/**
* When passing path to test to runners, you should join parts with this char.
* I.e.: file.py::PyClassTest::test_method
*/
protected static final String TEST_NAME_PARTS_SPLITTER = "::";
private String myPattern = ""; // pattern for modules in folder to match against
private boolean usePattern = false;
protected AbstractPythonTestRunConfiguration(Project project, ConfigurationFactory configurationFactory) {
super(project, configurationFactory);
}
@NotNull
@Override
public String getWorkingDirectorySafe() {
final String workingDirectoryFromConfig = getWorkingDirectory();
if (StringUtil.isNotEmpty(workingDirectoryFromConfig)) {
return workingDirectoryFromConfig;
}
final String folderName = myFolderName;
if (!StringUtil.isEmptyOrSpaces(folderName)) {
return folderName;
}
final String scriptName = myScriptName;
if (!StringUtil.isEmptyOrSpaces(scriptName)) {
final VirtualFile script = LocalFileSystem.getInstance().findFileByPath(scriptName);
if (script != null) {
return script.getParent().getPath();
}
}
return super.getWorkingDirectorySafe();
}
@Override
public void readExternal(Element element) throws InvalidDataException {
super.readExternal(element);
myScriptName = JDOMExternalizerUtil.readField(element, "SCRIPT_NAME");
myClassName = JDOMExternalizerUtil.readField(element, "CLASS_NAME");
myMethodName = JDOMExternalizerUtil.readField(element, "METHOD_NAME");
myFolderName = JDOMExternalizerUtil.readField(element, "FOLDER_NAME");
myPattern = JDOMExternalizerUtil.readField(element, "PATTERN");
usePattern = Boolean.parseBoolean(JDOMExternalizerUtil.readField(element, "USE_PATTERN"));
try {
final String testType = JDOMExternalizerUtil.readField(element, "TEST_TYPE");
myTestType = testType != null ? TestType.valueOf(testType) : TestType.TEST_SCRIPT;
}
catch (IllegalArgumentException e) {
myTestType = TestType.TEST_SCRIPT; // safe default
}
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
super.writeExternal(element);
JDOMExternalizerUtil.writeField(element, "SCRIPT_NAME", myScriptName);
JDOMExternalizerUtil.writeField(element, "CLASS_NAME", myClassName);
JDOMExternalizerUtil.writeField(element, "METHOD_NAME", myMethodName);
JDOMExternalizerUtil.writeField(element, "FOLDER_NAME", myFolderName);
JDOMExternalizerUtil.writeField(element, "TEST_TYPE", myTestType.toString());
JDOMExternalizerUtil.writeField(element, "PATTERN", myPattern);
JDOMExternalizerUtil.writeField(element, "USE_PATTERN", String.valueOf(usePattern));
}
public AbstractPythonRunConfigurationParams getBaseParams() {
return this;
}
public String getClassName() {
return myClassName;
}
public void setClassName(String className) {
myClassName = className;
}
public String getFolderName() {
return myFolderName;
}
public void setFolderName(String folderName) {
myFolderName = folderName;
}
public String getScriptName() {
return myScriptName;
}
public void setScriptName(String scriptName) {
myScriptName = scriptName;
}
public String getMethodName() {
return myMethodName;
}
public void setMethodName(String methodName) {
myMethodName = methodName;
}
public TestType getTestType() {
return myTestType;
}
public void setTestType(TestType testType) {
myTestType = testType;
}
public String getPattern() {
return myPattern;
}
public void setPattern(String pattern) {
myPattern = pattern;
}
public boolean usePattern() {
return usePattern;
}
public void usePattern(boolean usePattern) {
this.usePattern = usePattern;
}
public enum TestType {
TEST_FOLDER,
TEST_SCRIPT,
TEST_CLASS,
TEST_METHOD,
TEST_FUNCTION,}
@Override
public void checkConfiguration() throws RuntimeConfigurationException {
super.checkConfiguration();
if (StringUtil.isEmptyOrSpaces(myFolderName) && myTestType == TestType.TEST_FOLDER) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_folder_name"));
}
if (StringUtil.isEmptyOrSpaces(getScriptName()) && myTestType != TestType.TEST_FOLDER) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_script_name"));
}
if (StringUtil.isEmptyOrSpaces(myClassName) && (myTestType == TestType.TEST_METHOD || myTestType == TestType.TEST_CLASS)) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_class_name"));
}
if (StringUtil.isEmptyOrSpaces(myMethodName) && (myTestType == TestType.TEST_METHOD || myTestType == TestType.TEST_FUNCTION)) {
throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_method_name"));
}
}
public boolean compareSettings(AbstractPythonTestRunConfiguration cfg) {
if (cfg == null) return false;
if (getTestType() != cfg.getTestType()) return false;
switch (getTestType()) {
case TEST_FOLDER:
return getFolderName().equals(cfg.getFolderName());
case TEST_SCRIPT:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory());
case TEST_CLASS:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory()) &&
getClassName().equals(cfg.getClassName());
case TEST_METHOD:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory()) &&
getClassName().equals(cfg.getClassName()) &&
getMethodName().equals(cfg.getMethodName());
case TEST_FUNCTION:
return getScriptName().equals(cfg.getScriptName()) &&
getWorkingDirectory().equals(cfg.getWorkingDirectory()) &&
getMethodName().equals(cfg.getMethodName());
default:
throw new IllegalStateException("Unknown test type: " + getTestType());
}
}
public static void copyParams(AbstractPythonTestRunConfigurationParams source, AbstractPythonTestRunConfigurationParams target) {
AbstractPythonRunConfiguration.copyParams(source.getBaseParams(), target.getBaseParams());
target.setScriptName(source.getScriptName());
target.setClassName(source.getClassName());
target.setFolderName(source.getFolderName());
target.setMethodName(source.getMethodName());
target.setTestType(source.getTestType());
target.setPattern(source.getPattern());
target.usePattern(source.usePattern());
target.setAddContentRoots(source.shouldAddContentRoots());
target.setAddSourceRoots(source.shouldAddSourceRoots());
}
public AbstractPythonTestRunConfigurationParams getTestRunConfigurationParams() {
return this;
}
@Override
public String suggestedName() {
switch (myTestType) {
case TEST_CLASS:
return getPluralTitle() + " in " + myClassName;
case TEST_METHOD:
return getTitle() + " " + myClassName + "." + myMethodName;
case TEST_SCRIPT:
String name = new File(getScriptName()).getName();
name = StringUtil.trimEnd(name, ".py");
return getPluralTitle() + " in " + name;
case TEST_FOLDER:
String folderName = new File(myFolderName).getName();
return getPluralTitle() + " in " + folderName;
case TEST_FUNCTION:
return getTitle() + " " + myMethodName;
default:
throw new IllegalStateException("Unknown test type: " + myTestType);
}
protected AbstractPythonTestRunConfiguration(Project project, ConfigurationFactory factory) {
super(project, factory);
}
/**
* Create test spec (string to be passed to runner, probably glued with {@link AbstractPythonOldTestRunConfiguration#TEST_NAME_PARTS_SPLITTER})
*
* @param location test location as reported by runner
* @param failedTest failed test
* @return string spec or null if spec calculation is impossible
*/
@Nullable
@Override
public String getActionName() {
if (TestType.TEST_METHOD.equals(myTestType))
return getTitle() + " " + myMethodName;
return suggestedName();
}
protected abstract String getTitle();
protected abstract String getPluralTitle();
@Override
public RefactoringElementListener getRefactoringElementListener(PsiElement element) {
if (element instanceof PsiDirectory) {
VirtualFile vFile = ((PsiDirectory)element).getVirtualFile();
if ((myTestType == TestType.TEST_FOLDER && pathsEqual(vFile, myFolderName)) || pathsEqual(vFile, getWorkingDirectory())) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
String newPath = FileUtil.toSystemDependentName(((PsiDirectory)newElement).getVirtualFile().getPath());
setWorkingDirectory(newPath);
if (myTestType == TestType.TEST_FOLDER) {
myFolderName = newPath;
}
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
final String systemDependant = FileUtil.toSystemDependentName(oldQualifiedName);
setWorkingDirectory(systemDependant);
if (myTestType == TestType.TEST_FOLDER) {
myFolderName = systemDependant;
}
}
};
}
return null;
public String getTestSpec(@NotNull final Location<?> location, @NotNull final AbstractTestProxy failedTest) {
PsiElement element = location.getPsiElement();
PyClass pyClass = PsiTreeUtil.getParentOfType(element, PyClass.class, false);
if (location instanceof PyPsiLocationWithFixedClass) {
pyClass = ((PyPsiLocationWithFixedClass)location).getFixedClass();
}
if (myTestType == TestType.TEST_FOLDER) {
return null;
}
File scriptFile = new File(myScriptName);
if (!scriptFile.isAbsolute()) {
scriptFile = new File(getWorkingDirectory(), myScriptName);
}
PsiFile containingFile = element.getContainingFile();
VirtualFile vFile = containingFile == null ? null : containingFile.getVirtualFile();
if (vFile != null && Comparing.equal(new File(vFile.getPath()).getAbsolutePath(), scriptFile.getAbsolutePath())) {
if (element instanceof PsiFile) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
VirtualFile virtualFile = ((PsiFile)newElement).getVirtualFile();
if (virtualFile != null) {
myScriptName = FileUtil.toSystemDependentName(virtualFile.getPath());
}
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
myScriptName = FileUtil.toSystemDependentName(oldQualifiedName);
}
};
PyFunction pyFunction = PsiTreeUtil.getParentOfType(element, PyFunction.class, false);
final VirtualFile virtualFile = location.getVirtualFile();
if (virtualFile != null) {
String path = virtualFile.getCanonicalPath();
if (pyClass != null) {
path += TEST_NAME_PARTS_SPLITTER + pyClass.getName();
}
if (element instanceof PyClass && (myTestType == TestType.TEST_CLASS || myTestType == TestType.TEST_METHOD) &&
Comparing.equal(((PyClass)element).getName(), myClassName)) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
myClassName = ((PyClass) newElement).getName();
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
myClassName = oldQualifiedName;
}
};
}
if (element instanceof PyFunction &&
Comparing.equal(((PyFunction) element).getName(), myMethodName)) {
ScopeOwner scopeOwner = PsiTreeUtil.getParentOfType(element, ScopeOwner.class);
if ((myTestType == TestType.TEST_FUNCTION && scopeOwner instanceof PyFile) ||
(myTestType == TestType.TEST_METHOD && scopeOwner instanceof PyClass && Comparing.equal(scopeOwner.getName(), myClassName))) {
return new RefactoringElementAdapter() {
@Override
protected void elementRenamedOrMoved(@NotNull PsiElement newElement) {
myMethodName = ((PyFunction) newElement).getName();
}
@Override
public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) {
final int methodIdx = oldQualifiedName.indexOf("#") + 1;
if (methodIdx > 0 && methodIdx < oldQualifiedName.length()) {
myMethodName = oldQualifiedName.substring(methodIdx);
}
}
};
}
if (pyFunction != null) {
path += TEST_NAME_PARTS_SPLITTER + pyFunction.getName();
}
return path;
}
return null;
}
private static boolean pathsEqual(VirtualFile vFile, final String folderName) {
return Comparing.equal(new File(vFile.getPath()).getAbsolutePath(), new File(folderName).getAbsolutePath());
}
}
@@ -35,8 +35,8 @@ public interface AbstractPythonTestRunConfigurationParams {
String getMethodName();
void setMethodName(String methodName);
AbstractPythonTestRunConfiguration.TestType getTestType();
void setTestType(AbstractPythonTestRunConfiguration.TestType testType);
AbstractPythonOldTestRunConfiguration.TestType getTestType();
void setTestType(AbstractPythonOldTestRunConfiguration.TestType testType);
boolean usePattern();
void usePattern(boolean isPureUnittest);
@@ -0,0 +1,39 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing;
import com.intellij.execution.Location;
import com.intellij.execution.testframework.AbstractTestProxy;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* {@link AbstractPythonTestRunConfiguration} that handles failed test rerun by itself
*
* @author Ilya.Kazakevich
*/
@FunctionalInterface
public interface PyRerunAwareConfiguration {
/**
* Lunched each time user clicks "rerun". Must return new test specs.
*/
@NotNull
List<String> getTestSpecsForRerun(@NotNull GlobalSearchScope scope, @NotNull List<Pair<Location<?>, AbstractTestProxy>> locations);
}
@@ -31,6 +31,7 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComponentContainer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
@@ -74,7 +75,7 @@ public class PyRerunFailedTestsAction extends AbstractRerunFailedTestsAction {
@Nullable
@Override
public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException {
final AbstractPythonRunConfiguration configuration = ((AbstractPythonRunConfiguration)getPeer());
final AbstractPythonTestRunConfiguration<?> configuration = ((AbstractPythonTestRunConfiguration)getPeer());
// If configuration wants to take care about rerun itself
if (configuration instanceof TestRunConfigurationReRunResponsible) {
@@ -93,13 +94,13 @@ public class PyRerunFailedTestsAction extends AbstractRerunFailedTestsAction {
}
}
private class FailedPythonTestCommandLineStateBase extends PythonTestCommandLineStateBase {
private final PythonTestCommandLineStateBase myState;
private class FailedPythonTestCommandLineStateBase extends PythonTestCommandLineStateBase<AbstractPythonTestRunConfiguration<?>> {
private final PythonTestCommandLineStateBase<?> myState;
private final Project myProject;
public FailedPythonTestCommandLineStateBase(AbstractPythonRunConfiguration configuration,
public FailedPythonTestCommandLineStateBase(AbstractPythonTestRunConfiguration<?> configuration,
ExecutionEnvironment env,
PythonTestCommandLineStateBase state) {
PythonTestCommandLineStateBase<?> state) {
super(configuration, env);
myState = state;
myProject = configuration.getProject();
@@ -128,25 +129,38 @@ public class PyRerunFailedTestsAction extends AbstractRerunFailedTestsAction {
@NotNull
@Override
protected List<String> getTestSpecs() {
List<String> specs = new ArrayList<>();
List<AbstractTestProxy> failedTests = getFailedTests(myProject);
for (AbstractTestProxy failedTest : failedTests) {
final List<Pair<Location<?>, AbstractTestProxy>> failedTestLocations = new ArrayList<>();
final List<AbstractTestProxy> failedTests = getFailedTests(myProject);
for (final AbstractTestProxy failedTest : failedTests) {
if (failedTest.isLeaf()) {
final Location<?> location = failedTest.getLocation(myProject, myConsoleProperties.getScope());
if (location != null) {
final String spec = getConfiguration().getTestSpec(location, failedTest);
if (spec != null && !specs.contains(spec)) {
specs.add(spec);
}
failedTestLocations.add(Pair.create(location, failedTest));
}
}
}
if (specs.isEmpty()) {
final List<String> result;
final AbstractPythonTestRunConfiguration<?> configuration = getConfiguration();
if (configuration instanceof PyRerunAwareConfiguration) {
result = ((PyRerunAwareConfiguration)configuration).getTestSpecsForRerun(myConsoleProperties.getScope(), failedTestLocations);
}
else {
final Collection<String> locations = new LinkedHashSet<>();
locations.addAll(failedTestLocations.stream()
.map(o -> configuration.getTestSpec(o.first, o.second))
.filter(o -> o != null)
.collect(Collectors.toList()));
result = new ArrayList<>(locations);
}
if (result.isEmpty()) {
final List<String> locations = failedTests.stream().map(AbstractTestProxy::getLocationUrl).collect(Collectors.toList());
Logger.getInstance(FailedPythonTestCommandLineStateBase.class).warn(
String.format("Can't resolve specs for the following tests: %s", StringUtil.join(locations, ", ")));
}
return specs;
return result;
}
@Override
@@ -47,14 +47,14 @@ import java.util.Map;
/**
* @author yole
*/
public abstract class PythonTestCommandLineStateBase extends PythonCommandLineState {
protected final AbstractPythonRunConfiguration myConfiguration;
public abstract class PythonTestCommandLineStateBase<T extends AbstractPythonRunConfiguration<?>> extends PythonCommandLineState {
protected final T myConfiguration;
public AbstractPythonRunConfiguration<?> getConfiguration() {
public T getConfiguration() {
return myConfiguration;
}
public PythonTestCommandLineStateBase(AbstractPythonRunConfiguration configuration, ExecutionEnvironment env) {
public PythonTestCommandLineStateBase(T configuration, ExecutionEnvironment env) {
super(configuration, env);
myConfiguration = configuration;
setRunWithPty(false);
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,125 +15,25 @@
*/
package com.jetbrains.python.testing;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.ConfigurationType;
import com.intellij.execution.configurations.ConfigurationTypeUtil;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.openapi.project.Project;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.run.PythonConfigurationFactoryBase;
import com.jetbrains.python.testing.doctest.PythonDocTestRunConfiguration;
import com.jetbrains.python.testing.nosetest.PythonNoseTestRunConfiguration;
import com.jetbrains.python.testing.pytest.PyTestRunConfiguration;
import com.jetbrains.python.testing.unittest.PythonUnitTestRunConfiguration;
import icons.PythonIcons;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
/**
* User : catherine
* @author Ilya.Kazakevich
*/
public class PythonTestConfigurationType implements ConfigurationType {
public static final String ID = "tests";
public final PythonDocTestConfigurationFactory PY_DOCTEST_FACTORY = new PythonDocTestConfigurationFactory(this);
public final PythonUnitTestConfigurationFactory PY_UNITTEST_FACTORY = new PythonUnitTestConfigurationFactory(this);
public final PythonNoseTestConfigurationFactory PY_NOSETEST_FACTORY = new PythonNoseTestConfigurationFactory(this);
public final PythonPyTestConfigurationFactory PY_PYTEST_FACTORY = new PythonPyTestConfigurationFactory(this);
public static PythonTestConfigurationType getInstance() {
return ConfigurationTypeUtil.findConfigurationType(PythonTestConfigurationType.class);
}
private static class PythonUnitTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonUnitTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PythonUnitTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.unittest.display_name");
}
}
private static class PythonDocTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonDocTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PythonDocTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.doctest.display_name");
}
}
private static class PythonPyTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonPyTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PyTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.pytest.display_name");
}
}
private static class PythonNoseTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonNoseTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PythonNoseTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.nosetests.display_name");
}
}
@Override
public abstract class PythonTestConfigurationType implements ConfigurationType {
public String getDisplayName() {
return PyBundle.message("runcfg.test.display_name");
}
@Override
public String getConfigurationTypeDescription() {
return PyBundle.message("runcfg.test.description");
}
@Override
public Icon getIcon() {
return PythonIcons.Python.PythonTests;
}
@NotNull
@Override
public String getId() {
return ID;
}
@Override
public ConfigurationFactory[] getConfigurationFactories() {
return new ConfigurationFactory[] {PY_UNITTEST_FACTORY, PY_DOCTEST_FACTORY, PY_NOSETEST_FACTORY,
PY_PYTEST_FACTORY};
}
}
@@ -39,9 +39,9 @@ import com.jetbrains.python.PythonModuleTypeBase;
import com.jetbrains.python.facet.PythonFacetSettings;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
import com.jetbrains.python.run.PythonRunConfigurationProducer;
import com.jetbrains.python.testing.unittest.PythonUnitTestRunConfiguration;
import com.jetbrains.python.testing.universalTests.PyUniversalTestsKt;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -54,14 +54,14 @@ import java.util.List;
/**
* User: ktisha
*/
abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRunConfiguration<T>> extends RunConfigurationProducer<AbstractPythonTestRunConfiguration<T>> {
abstract public class PythonTestOldConfigurationProducer<T extends AbstractPythonOldTestRunConfiguration<T>> extends RunConfigurationProducer<AbstractPythonOldTestRunConfiguration<T>> {
public PythonTestConfigurationProducer(final ConfigurationFactory configurationFactory) {
public PythonTestOldConfigurationProducer(final ConfigurationFactory configurationFactory) {
super(configurationFactory);
}
@Override
public boolean isConfigurationFromContext(AbstractPythonTestRunConfiguration configuration, ConfigurationContext context) {
public boolean isConfigurationFromContext(AbstractPythonOldTestRunConfiguration configuration, ConfigurationContext context) {
final Location location = context.getLocation();
if (location == null || !isAvailable(location)) return false;
final PsiElement element = location.getPsiElement();
@@ -73,12 +73,12 @@ abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRu
final PyFunction pyFunction = PsiTreeUtil.getParentOfType(element, PyFunction.class, false);
final PyClass pyClass = PsiTreeUtil.getParentOfType(element, PyClass.class);
final AbstractPythonTestRunConfiguration.TestType confType = configuration.getTestType();
final AbstractPythonOldTestRunConfiguration.TestType confType = configuration.getTestType();
final String workingDirectory = configuration.getWorkingDirectory();
if (element instanceof PsiDirectory) {
final String path = ((PsiDirectory)element).getVirtualFile().getPath();
return confType == AbstractPythonTestRunConfiguration.TestType.TEST_FOLDER &&
return confType == AbstractPythonOldTestRunConfiguration.TestType.TEST_FOLDER &&
path.equals(configuration.getFolderName()) ||
path.equals(new File(workingDirectory, configuration.getFolderName()).getAbsolutePath());
}
@@ -90,27 +90,27 @@ abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRu
if (pyFunction != null) {
final String methodName = configuration.getMethodName();
if (pyFunction.getContainingClass() == null) {
return confType == AbstractPythonTestRunConfiguration.TestType.TEST_FUNCTION &&
return confType == AbstractPythonOldTestRunConfiguration.TestType.TEST_FUNCTION &&
methodName.equals(pyFunction.getName()) && isTestFileEquals;
}
else {
final String className = configuration.getClassName();
return confType == AbstractPythonTestRunConfiguration.TestType.TEST_METHOD &&
return confType == AbstractPythonOldTestRunConfiguration.TestType.TEST_METHOD &&
methodName.equals(pyFunction.getName()) &&
pyClass != null && className.equals(pyClass.getName()) && isTestFileEquals;
}
}
if (pyClass != null) {
final String className = configuration.getClassName();
return confType == AbstractPythonTestRunConfiguration.TestType.TEST_CLASS &&
return confType == AbstractPythonOldTestRunConfiguration.TestType.TEST_CLASS &&
className.equals(pyClass.getName()) && isTestFileEquals;
}
return confType == AbstractPythonTestRunConfiguration.TestType.TEST_SCRIPT && isTestFileEquals;
return confType == AbstractPythonOldTestRunConfiguration.TestType.TEST_SCRIPT && isTestFileEquals;
}
@Override
protected boolean setupConfigurationFromContext(AbstractPythonTestRunConfiguration<T> configuration,
protected boolean setupConfigurationFromContext(AbstractPythonOldTestRunConfiguration<T> configuration,
ConfigurationContext context,
Ref<PsiElement> sourceElement) {
if (context == null) return false;
@@ -147,12 +147,12 @@ abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRu
}
private boolean setupConfigurationFromFolder(@NotNull final PsiDirectory element,
@NotNull final AbstractPythonTestRunConfiguration configuration) {
@NotNull final AbstractPythonOldTestRunConfiguration configuration) {
final VirtualFile virtualFile = element.getVirtualFile();
if (!isTestFolder(virtualFile, element.getProject())) return false;
final String path = virtualFile.getPath();
configuration.setTestType(AbstractPythonTestRunConfiguration.TestType.TEST_FOLDER);
configuration.setTestType(AbstractPythonOldTestRunConfiguration.TestType.TEST_FOLDER);
configuration.setFolderName(path);
configuration.setWorkingDirectory(path);
configuration.setGeneratedName();
@@ -160,40 +160,40 @@ abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRu
return true;
}
private static void setModuleSdk(@NotNull final PsiElement element, @NotNull final AbstractPythonTestRunConfiguration configuration) {
private static void setModuleSdk(@NotNull final PsiElement element, @NotNull final AbstractPythonOldTestRunConfiguration configuration) {
configuration.setUseModuleSdk(true);
configuration.setModule(ModuleUtilCore.findModuleForPsiElement(element));
}
protected boolean setupConfigurationFromFunction(@NotNull final PyFunction pyFunction,
@NotNull final AbstractPythonTestRunConfiguration configuration) {
@NotNull final AbstractPythonOldTestRunConfiguration configuration) {
final PyClass containingClass = pyFunction.getContainingClass();
configuration.setMethodName(pyFunction.getName());
if (containingClass != null) {
configuration.setClassName(containingClass.getName());
configuration.setTestType(AbstractPythonTestRunConfiguration.TestType.TEST_METHOD);
configuration.setTestType(AbstractPythonOldTestRunConfiguration.TestType.TEST_METHOD);
}
else {
configuration.setTestType(AbstractPythonTestRunConfiguration.TestType.TEST_FUNCTION);
configuration.setTestType(AbstractPythonOldTestRunConfiguration.TestType.TEST_FUNCTION);
}
return setupConfigurationScript(configuration, pyFunction);
}
protected boolean setupConfigurationFromClass(@NotNull final PyClass pyClass,
@NotNull final AbstractPythonTestRunConfiguration configuration) {
configuration.setTestType(AbstractPythonTestRunConfiguration.TestType.TEST_CLASS);
@NotNull final AbstractPythonOldTestRunConfiguration configuration) {
configuration.setTestType(AbstractPythonOldTestRunConfiguration.TestType.TEST_CLASS);
configuration.setClassName(pyClass.getName());
return setupConfigurationScript(configuration, pyClass);
}
protected boolean setupConfigurationFromFile(@NotNull final PyFile pyFile,
@NotNull final AbstractPythonTestRunConfiguration configuration) {
configuration.setTestType(AbstractPythonTestRunConfiguration.TestType.TEST_SCRIPT);
@NotNull final AbstractPythonOldTestRunConfiguration configuration) {
configuration.setTestType(AbstractPythonOldTestRunConfiguration.TestType.TEST_SCRIPT);
return setupConfigurationScript(configuration, pyFile);
}
protected static boolean setupConfigurationScript(@NotNull final AbstractPythonTestRunConfiguration cfg,
protected static boolean setupConfigurationScript(@NotNull final AbstractPythonOldTestRunConfiguration cfg,
@NotNull final PyElement element) {
final PyFile containingFile = PyUtil.getContainingPyFile(element);
if (containingFile == null) return false;
@@ -227,12 +227,12 @@ abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRu
}
protected boolean isTestClass(@NotNull final PyClass pyClass,
@Nullable final AbstractPythonTestRunConfiguration configuration, @Nullable final TypeEvalContext context) {
@Nullable final AbstractPythonOldTestRunConfiguration configuration, @Nullable final TypeEvalContext context) {
return PythonUnitTestUtil.isTestCaseClass(pyClass, context);
}
protected boolean isTestFunction(@NotNull final PyFunction pyFunction,
@Nullable final AbstractPythonTestRunConfiguration configuration) {
@Nullable final AbstractPythonOldTestRunConfiguration configuration) {
return PythonUnitTestUtil.isTestCaseFunction(pyFunction);
}
@@ -264,11 +264,14 @@ abstract public class PythonTestConfigurationProducer<T extends AbstractPythonRu
@Override
public boolean isPreferredConfiguration(ConfigurationFromContext self, ConfigurationFromContext other) {
if (PyUniversalTestsKt.isUniversalModeEnabled()) {
return false;
}
final RunConfiguration configuration = self.getConfiguration();
if (configuration instanceof PythonUnitTestRunConfiguration &&
((PythonUnitTestRunConfiguration)configuration).getTestType() == AbstractPythonTestRunConfiguration.TestType.TEST_FOLDER) {
((AbstractPythonTestRunConfigurationParams)configuration).getTestType() == AbstractPythonOldTestRunConfiguration.TestType.TEST_FOLDER) {
return true;
}
return other.isProducedBy(PythonTestConfigurationProducer.class) || other.isProducedBy(PythonRunConfigurationProducer.class);
return other.isProducedBy(PythonTestOldConfigurationProducer.class) || other.isProducedBy(PythonRunConfigurationProducer.class);
}
}
@@ -0,0 +1,134 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.ConfigurationType;
import com.intellij.execution.configurations.ConfigurationTypeUtil;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.openapi.project.Project;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.run.PythonConfigurationFactoryBase;
import com.jetbrains.python.testing.doctest.PythonDocTestRunConfiguration;
import com.jetbrains.python.testing.nosetest.PythonNoseTestRunConfiguration;
import com.jetbrains.python.testing.pytest.PyTestRunConfiguration;
import com.jetbrains.python.testing.unittest.PythonUnitTestRunConfiguration;
import com.jetbrains.python.testing.universalTests.PyUniversalTestsKt;
import org.jetbrains.annotations.NotNull;
/**
* User : catherine
*/
public class PythonTestOldConfigurationType extends PythonTestConfigurationType {
public static final String ID = "tests";
public final PythonDocTestConfigurationFactory PY_DOCTEST_FACTORY = new PythonDocTestConfigurationFactory(this);
public final PythonUnitTestConfigurationFactory PY_UNITTEST_FACTORY = new PythonUnitTestConfigurationFactory(this);
public final PythonNoseTestConfigurationFactory PY_NOSETEST_FACTORY = new PythonNoseTestConfigurationFactory(this);
public final PythonPyTestConfigurationFactory PY_PYTEST_FACTORY = new PythonPyTestConfigurationFactory(this);
public static PythonTestOldConfigurationType getInstance() {
return ConfigurationTypeUtil.findConfigurationType(PythonTestOldConfigurationType.class);
}
@Override
public String getDisplayName() {
// Only doctests are supported when new mode (isUniversalModeEnabled)
return (PyUniversalTestsKt.isUniversalModeEnabled()
? super.getDisplayName() + ' ' + PY_DOCTEST_FACTORY.getName()
: super.getDisplayName());
}
private static class PythonUnitTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonUnitTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PythonUnitTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.unittest.display_name");
}
}
private static class PythonDocTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonDocTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PythonDocTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.doctest.display_name");
}
}
private static class PythonPyTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonPyTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PyTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.pytest.display_name");
}
}
private static class PythonNoseTestConfigurationFactory extends PythonConfigurationFactoryBase {
protected PythonNoseTestConfigurationFactory(ConfigurationType configurationType) {
super(configurationType);
}
@Override
public RunConfiguration createTemplateConfiguration(Project project) {
return new PythonNoseTestRunConfiguration(project, this);
}
@Override
public String getName() {
return PyBundle.message("runcfg.nosetests.display_name");
}
}
@NotNull
@Override
public String getId() {
return ID;
}
@Override
public ConfigurationFactory[] getConfigurationFactories() {
if (PyUniversalTestsKt.isUniversalModeEnabled()) {
return new ConfigurationFactory[]{PY_DOCTEST_FACTORY};
}
return new ConfigurationFactory[]{PY_UNITTEST_FACTORY, PY_DOCTEST_FACTORY, PY_NOSETEST_FACTORY,
PY_PYTEST_FACTORY};
}
}
@@ -68,7 +68,7 @@ public class PythonTestRunConfigurationForm implements AbstractPythonTestRunConf
private boolean myPatternIsVisible = true;
public PythonTestRunConfigurationForm(final Project project,
final AbstractPythonTestRunConfiguration configuration) {
final AbstractPythonOldTestRunConfiguration configuration) {
myProject = project;
myCommonOptionsForm = PyCommonOptionsFormFactory.getInstance().createForm(configuration.getCommonOptionsFormData());
myCommonOptionsPlaceholder.add(myCommonOptionsForm.getMainPanel(), BorderLayout.CENTER);
@@ -243,8 +243,8 @@ public class PythonTestRunConfigurationForm implements AbstractPythonTestRunConf
myTestMethodComponent.getLabel().setText(testType == TestType.TEST_METHOD? PyBundle.message("runcfg.unittest.dlg.method_label")
: PyBundle.message("runcfg.unittest.dlg.function_label"));
if (myPatternIsVisible) {
myPatternTextField.setVisible(getTestType() == AbstractPythonTestRunConfiguration.TestType.TEST_FOLDER);
myPatternCheckBox.setVisible(getTestType() == AbstractPythonTestRunConfiguration.TestType.TEST_FOLDER);
myPatternTextField.setVisible(getTestType() == AbstractPythonOldTestRunConfiguration.TestType.TEST_FOLDER);
myPatternCheckBox.setVisible(getTestType() == AbstractPythonOldTestRunConfiguration.TestType.TEST_FOLDER);
}
}
@@ -20,7 +20,7 @@ import com.intellij.conversion.CannotConvertException;
import com.intellij.conversion.ConversionProcessor;
import com.intellij.conversion.RunManagerSettings;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import org.jdom.Element;
/**
@@ -50,7 +50,7 @@ public class PythonTestRunConfigurationsConverter extends ConversionProcessor<Ru
final String confType = element.getAttributeValue("type");
final String factoryName = ourTypeToFactoryNameMap.get(confType);
if (factoryName != null) {
element.setAttribute("type", PythonTestConfigurationType.ID);
element.setAttribute("type", PythonTestOldConfigurationType.ID);
element.setAttribute("factoryName", factoryName);
}
}
@@ -31,28 +31,28 @@ import com.jetbrains.python.psi.PyElement;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.PythonTestConfigurationProducer;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import com.jetbrains.python.testing.PythonTestOldConfigurationProducer;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class PythonDocTestConfigurationProducer extends PythonTestConfigurationProducer {
public class PythonDocTestConfigurationProducer extends PythonTestOldConfigurationProducer {
public PythonDocTestConfigurationProducer() {
super(PythonTestConfigurationType.getInstance().PY_DOCTEST_FACTORY);
super(PythonTestOldConfigurationType.getInstance().PY_DOCTEST_FACTORY);
}
@Override
protected boolean isTestFunction(@NotNull final PyFunction pyFunction, @Nullable final AbstractPythonTestRunConfiguration configuration) {
protected boolean isTestFunction(@NotNull final PyFunction pyFunction, @Nullable final AbstractPythonOldTestRunConfiguration configuration) {
return PythonDocTestUtil.isDocTestFunction(pyFunction);
}
@Override
protected boolean isTestClass(@NotNull PyClass pyClass,
@Nullable final AbstractPythonTestRunConfiguration configuration,
@Nullable final AbstractPythonOldTestRunConfiguration configuration,
@Nullable final TypeEvalContext context) {
return PythonDocTestUtil.isDocTestClass(pyClass);
}
@@ -23,14 +23,14 @@ import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
/**
* User: catherine
*/
public class PythonDocTestRunConfiguration extends AbstractPythonTestRunConfiguration<PythonDocTestRunConfiguration>
public class PythonDocTestRunConfiguration extends AbstractPythonOldTestRunConfiguration<PythonDocTestRunConfiguration>
implements PythonDocTestRunConfigurationParams {
protected String myPluralTitle = "Doctests";
protected String myTitle = "Doctest";
@@ -27,9 +27,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PythonNoseTestConfigurationProducer extends
PythonTestConfigurationProducer {
PythonTestOldConfigurationProducer {
public PythonNoseTestConfigurationProducer() {
super(PythonTestConfigurationType.getInstance().PY_NOSETEST_FACTORY);
super(PythonTestOldConfigurationType.getInstance().PY_NOSETEST_FACTORY);
}
protected boolean isAvailable(@NotNull final Location location) {
@@ -45,7 +45,7 @@ public class PythonNoseTestConfigurationProducer extends
}
@Override
protected boolean isTestFunction(@NotNull final PyFunction pyFunction, @Nullable final AbstractPythonTestRunConfiguration configuration) {
protected boolean isTestFunction(@NotNull final PyFunction pyFunction, @Nullable final AbstractPythonOldTestRunConfiguration configuration) {
return PythonUnitTestUtil.isTestCaseFunction(pyFunction, true);
}
}
@@ -26,7 +26,7 @@ import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import com.jetbrains.python.testing.VFSTestFrameworkListener;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
@@ -34,7 +34,7 @@ import org.jetbrains.annotations.NotNull;
/**
* User: catherine
*/
public class PythonNoseTestRunConfiguration extends AbstractPythonTestRunConfiguration<PythonNoseTestRunConfiguration>
public class PythonNoseTestRunConfiguration extends AbstractPythonOldTestRunConfiguration<PythonNoseTestRunConfiguration>
implements PythonNoseTestRunConfigurationParams {
private String myParams = ""; // parameters for nosetests
protected String myTitle = "Nosetest";
@@ -80,7 +80,7 @@ public class PythonNoseTestRunConfiguration extends AbstractPythonTestRunConfigu
}
public static void copyParams(PythonNoseTestRunConfigurationParams source, PythonNoseTestRunConfigurationParams target) {
AbstractPythonTestRunConfiguration.copyParams(source.getTestRunConfigurationParams(), target.getTestRunConfigurationParams());
AbstractPythonOldTestRunConfiguration.copyParams(source.getTestRunConfigurationParams(), target.getTestRunConfigurationParams());
target.setParams(source.getParams());
target.useParam(source.useParam());
}
@@ -44,14 +44,14 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
public class PyTestConfigurationProducer extends PythonTestConfigurationProducer<PyTestRunConfiguration> {
public class PyTestConfigurationProducer extends PythonTestOldConfigurationProducer<PyTestRunConfiguration> {
public PyTestConfigurationProducer() {
super(PythonTestConfigurationType.getInstance().PY_PYTEST_FACTORY);
super(PythonTestOldConfigurationType.getInstance().PY_PYTEST_FACTORY);
}
@Override
protected boolean setupConfigurationFromContext(AbstractPythonTestRunConfiguration<PyTestRunConfiguration> configuration,
protected boolean setupConfigurationFromContext(AbstractPythonOldTestRunConfiguration<PyTestRunConfiguration> configuration,
ConfigurationContext context,
Ref<PsiElement> sourceElement) {
final PsiElement element = sourceElement.get();
@@ -144,7 +144,7 @@ public class PyTestConfigurationProducer extends PythonTestConfigurationProducer
}
@Override
public boolean isConfigurationFromContext(AbstractPythonTestRunConfiguration configuration, ConfigurationContext context) {
public boolean isConfigurationFromContext(AbstractPythonOldTestRunConfiguration configuration, ConfigurationContext context) {
final Location location = context.getLocation();
if (location == null) return false;
if (!(configuration instanceof PyTestRunConfiguration)) return false;
@@ -33,7 +33,7 @@ import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import com.jetbrains.python.testing.VFSTestFrameworkListener;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
@@ -45,7 +45,8 @@ import java.util.List;
/**
* @author yole
*/
public class PyTestRunConfiguration extends AbstractPythonTestRunConfiguration<PyTestRunConfiguration> implements PyTestRunConfigurationParams {
public class PyTestRunConfiguration extends AbstractPythonOldTestRunConfiguration<PyTestRunConfiguration>
implements PyTestRunConfigurationParams {
private String myTestToRun = "";
private String myKeywords = "";
private String myParams = "";
@@ -31,7 +31,7 @@ import com.intellij.util.ArrayUtil;
import com.intellij.util.xmlb.SkipEmptySerializationFilter;
import com.intellij.util.xmlb.XmlSerializer;
import com.intellij.util.xmlb.annotations.Tag;
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.serialization.AnnotationSerializationFilter;
import com.jetbrains.serialization.CompoundFilter;
import org.jdom.Element;
@@ -43,7 +43,7 @@ import java.util.List;
/**
* @author Ilya.Kazakevich
*/
public final class PyToxConfiguration extends AbstractPythonRunConfiguration<PyToxConfiguration> {
public final class PyToxConfiguration extends AbstractPythonTestRunConfiguration<PyToxConfiguration> {
@NotNull
private final Project myProject;
@@ -23,7 +23,7 @@ import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonHelper;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import com.jetbrains.python.testing.PythonTestCommandLineStateBase;
import com.jetbrains.python.testing.PythonUnitTestTestIdUrlProvider;
import org.jetbrains.annotations.NotNull;
@@ -45,8 +45,8 @@ public class PythonUnitTestCommandLineState extends PythonTestCommandLineStateBa
@Override
protected PythonHelper getRunner() {
if (myConfig.getTestType() == AbstractPythonTestRunConfiguration.TestType.TEST_SCRIPT &&
myConfig.getScriptName().endsWith(PyNames.SETUP_DOT_PY))
if (myConfig.getTestType() == AbstractPythonOldTestRunConfiguration.TestType.TEST_SCRIPT &&
myConfig.getScriptName().endsWith(PyNames.SETUP_DOT_PY))
return PythonHelper.SETUPPY;
return PythonHelper.UT;
}
@@ -98,7 +98,7 @@ public class PythonUnitTestCommandLineState extends PythonTestCommandLineStateBa
if (myConfig.useParam() && !StringUtil.isEmptyOrSpaces(myConfig.getParams()))
script_params.addParameter(myConfig.getParams());
if (myConfig.getTestType() != AbstractPythonTestRunConfiguration.TestType.TEST_SCRIPT ||
if (myConfig.getTestType() != AbstractPythonOldTestRunConfiguration.TestType.TEST_SCRIPT ||
!myConfig.getScriptName().endsWith(PyNames.SETUP_DOT_PY))
script_params.addParameter(String.valueOf(myConfig.isPureUnittest()));
}
@@ -36,9 +36,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
public class PythonUnitTestConfigurationProducer extends PythonTestConfigurationProducer {
public class PythonUnitTestConfigurationProducer extends PythonTestOldConfigurationProducer {
public PythonUnitTestConfigurationProducer() {
super(PythonTestConfigurationType.getInstance().PY_UNITTEST_FACTORY);
super(PythonTestOldConfigurationType.getInstance().PY_UNITTEST_FACTORY);
}
protected boolean isAvailable(@NotNull final Location location) {
@@ -54,7 +54,7 @@ public class PythonUnitTestConfigurationProducer extends PythonTestConfiguration
@Override
protected boolean isTestFunction(@NotNull final PyFunction pyFunction,
@Nullable final AbstractPythonTestRunConfiguration configuration) {
@Nullable final AbstractPythonOldTestRunConfiguration configuration) {
final boolean isTestFunction = super.isTestFunction(pyFunction, configuration);
return isTestFunction || (configuration instanceof PythonUnitTestRunConfiguration &&
!((PythonUnitTestRunConfiguration)configuration).isPureUnittest());
@@ -62,7 +62,7 @@ public class PythonUnitTestConfigurationProducer extends PythonTestConfiguration
@Override
protected boolean isTestClass(@NotNull PyClass pyClass,
@Nullable final AbstractPythonTestRunConfiguration configuration,
@Nullable final AbstractPythonOldTestRunConfiguration configuration,
TypeEvalContext context) {
final boolean isTestClass = super.isTestClass(pyClass, configuration, context);
return isTestClass || (configuration instanceof PythonUnitTestRunConfiguration &&
@@ -18,14 +18,13 @@ package com.jetbrains.python.testing.unittest;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.JDOMExternalizerUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
@@ -33,7 +32,7 @@ import org.jetbrains.annotations.NotNull;
* @author Leonid Shalupov
*/
public class PythonUnitTestRunConfiguration extends
AbstractPythonTestRunConfiguration<PythonUnitTestRunConfiguration>
AbstractPythonOldTestRunConfiguration<PythonUnitTestRunConfiguration>
implements PythonUnitTestRunConfigurationParams {
private boolean myIsPureUnittest = true;
protected String myTitle = "Unittest";
@@ -17,7 +17,7 @@ package com.jetbrains.python.testing.unittest;
import com.intellij.openapi.project.Project;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import com.jetbrains.python.testing.AbstractPythonTestRunConfigurationParams;
import com.jetbrains.python.testing.PythonTestRunConfigurationForm;
@@ -45,7 +45,7 @@ public class PythonUnitTestRunConfigurationForm implements PythonUnitTestRunConf
final ActionListener testTypeListener = new ActionListener() {
public void actionPerformed(final ActionEvent e) {
myIsPureUnittest.setVisible(myTestRunConfigurationForm.getTestType() != AbstractPythonTestRunConfiguration.TestType.TEST_FUNCTION);
myIsPureUnittest.setVisible(myTestRunConfigurationForm.getTestType() != AbstractPythonOldTestRunConfiguration.TestType.TEST_FUNCTION);
}
};
myTestRunConfigurationForm.addTestTypeListener(testTypeListener);
@@ -0,0 +1,55 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing.universalTests
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.testing.PythonTestConfigurationsModel
/**
* Nose runner
*/
class PyUniversalNoseTestSettingsEditor(configuration: PyUniversalTestConfiguration) :
PyUniversalTestSettingsEditor(PyUniversalTestForm.create(configuration))
class PyUniversalNoseTestExecutionEnvironment(configuration: PyUniversalNoseTestConfiguration, environment: ExecutionEnvironment) :
PyUniversalTestExecutionEnvironment<PyUniversalNoseTestConfiguration>(configuration, environment) {
override fun getRunner() = PythonHelper.UNIVERSAL_NOSE
}
class PyUniversalNoseTestConfiguration(project: Project, factory: PyUniversalTestFactory) : PyUniversalTestConfiguration(project, factory) {
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? =
PyUniversalNoseTestExecutionEnvironment(this, environment)
override fun createConfigurationEditor(): SettingsEditor<PyUniversalTestConfiguration> =
PyUniversalNoseTestSettingsEditor(this)
}
object PyUniversalNoseTestFactory : PyUniversalTestFactory() {
override fun createTemplateConfiguration(project: Project) = PyUniversalNoseTestConfiguration(project, this)
override fun getName(): String = PythonTestConfigurationsModel.PYTHONS_NOSETEST_NAME
}
@@ -0,0 +1,60 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing.universalTests
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.testing.PythonTestConfigurationsModel
/**
* Py.test runner
*/
class PyUniversalPyTestSettingsEditor(configuration: PyUniversalTestConfiguration) :
PyUniversalTestSettingsEditor(PyUniversalTestForm.create(configuration, PyUniversalPyTestConfiguration::keywords.name))
class PyUniversalPyTestExecutionEnvironment(configuration: PyUniversalPyTestConfiguration, environment: ExecutionEnvironment) :
PyUniversalTestExecutionEnvironment<PyUniversalPyTestConfiguration>(configuration, environment) {
override fun getRunner() = PythonHelper.UNIVERSAL_PYTEST
}
class PyUniversalPyTestConfiguration(project: Project, factory: PyUniversalTestFactory) : PyUniversalTestConfiguration(project, factory) {
@ConfigField
var keywords = ""
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? =
PyUniversalPyTestExecutionEnvironment(this, environment)
override fun createConfigurationEditor(): SettingsEditor<PyUniversalTestConfiguration> =
PyUniversalPyTestSettingsEditor(this)
override fun getCustomRawArgumentsString(): String =
when {
keywords.isEmpty() -> ""
else -> "-k $keywords"
}
}
object PyUniversalPyTestFactory : PyUniversalTestFactory() {
override fun createTemplateConfiguration(project: Project) = PyUniversalPyTestConfiguration(project, this)
override fun getName(): String = PythonTestConfigurationsModel.PY_TEST_NAME
}
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.testing.universalTests.PyUniversalTestForm">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties>
<background color="-12632257"/>
<enabled value="false"/>
</properties>
<border type="none"/>
<children>
<grid id="a3ef6" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="1" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="5429a" class="com.intellij.ui.components.JBLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Target"/>
</properties>
</component>
<grid id="7fd1b" binding="myTargets" layout-manager="FlowLayout" hgap="5" vgap="5" flow-align="1">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="8" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
<component id="43f5" class="com.intellij.ui.components.JBLabel" binding="myLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="0" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
</children>
</grid>
<vspacer id="f8783">
<constraints>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<grid id="a759e" binding="myOptionsPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
<component id="2e74" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myTargetText">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
<grid id="c2059" binding="myCustomOptionsPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<background color="-12828863"/>
</properties>
<border type="none"/>
<children/>
</grid>
</children>
</grid>
</form>
@@ -0,0 +1,207 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing.universalTests;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBRadioButton;
import com.intellij.ui.components.JBTextField;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.util.ArrayUtil;
import com.jetbrains.python.PyFileChooserDescriptor;
import com.jetbrains.python.run.AbstractPyCommonOptionsForm;
import com.jetbrains.python.run.PyCommonOptionsFormFactory;
import com.jetbrains.reflection.ReflectionUtilsKt;
import com.jetbrains.reflection.SimplePropertiesProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
import java.util.regex.Pattern;
/**
* Form to display run configuration.
* It displays target type, target, additional arguments, custom options (if provided) and environment options
* Create with {@link #create(PyUniversalTestConfiguration, String...)}
*
* @author Ilya.Kazakevich
*/
public final class PyUniversalTestForm implements SimplePropertiesProvider {
/**
* Regex to convert additionalArgumentNames to "Additional Argument Names"
*/
private static final Pattern CAPITAL_LETTER = Pattern.compile("(?=\\p{Upper})");
private JPanel myPanel;
/**
* Panel for test targets
*/
private JPanel myTargets;
/**
* Panel for environment options
*/
private JPanel myOptionsPanel;
private TextFieldWithBrowseButton myTargetText;
/**
* Test label
*/
private JBLabel myLabel;
/**
* Panel for custom options, specific for runner and for "Additional Arguments"al;sop
*/
private JPanel myCustomOptionsPanel;
private final ButtonGroup myButtonGroup = new ButtonGroup();
private AbstractPyCommonOptionsForm myOptionsForm;
private final Map<String, JBTextField> myCustomOptions = new LinkedHashMap<>(); // TDO: Linked -- order
@NotNull
JPanel getPanel() {
return myPanel;
}
@NotNull
@Override
public List<String> getPropertyNames() {
return new ArrayList<>(myCustomOptions.keySet());
}
@Override
public void setPropertyValue(@NotNull final String propertyName, @Nullable final String propertyValue) {
myCustomOptions.get(propertyName).setText(propertyValue != null ? propertyValue : "");
}
@Nullable
@Override
public String getPropertyValue(@NotNull final String propertyName) {
return myCustomOptions.get(propertyName).getText();
}
private PyUniversalTestForm() {
}
/**
* @param configuration configuration to configure form on creation
* @param customOptions additional option names this form shall support. Make sure your configuration has appropriate properties.
*/
@NotNull
public static PyUniversalTestForm create(@NotNull final PyUniversalTestConfiguration configuration,
@NotNull final String... customOptions) { // TODO: DOC
final PyUniversalTestForm form = new PyUniversalTestForm();
form.myTargetText.addBrowseFolderListener("Choose File or Folder", null, configuration.getProject(),
new PyFileChooserDescriptor(true));
for (final TestTargetType testTargetType : TestTargetType.values()) {
final JBRadioButton button = new JBRadioButton(StringUtil.capitalize(testTargetType.name().toLowerCase(Locale.getDefault())));
button.setActionCommand(testTargetType.name());
button.addActionListener(o -> form.configureElementsVisibility());
form.myButtonGroup.add(button);
form.myTargets.add(button);
}
form.myButtonGroup.getElements().nextElement().setSelected(true);
form.myOptionsForm = PyCommonOptionsFormFactory.getInstance().createForm(configuration.getCommonOptionsFormData());
final GridConstraints constraints = new GridConstraints();
constraints.setFill(GridConstraints.FILL_BOTH);
form.myOptionsPanel.add(form.myOptionsForm.getMainPanel(), constraints);
form.myLabel.setText(configuration.getTestFrameworkName());
form.addCustomOptions(ArrayUtil.mergeArrays(customOptions, PyUniversalTestsKt.getAdditionalArgumentsPropertyName()));
configuration.copyTo(ReflectionUtilsKt.getProperties(form, null, true));
return form;
}
private void addCustomOptions(@NotNull final String... optionNames) {
if (optionNames.length == 0) {
return;
}
for (final String optionName : optionNames) {
final JBTextField textField = new JBTextField();
myCustomOptions.put(optionName, textField);
}
myCustomOptionsPanel.setLayout(new GridLayoutManager(optionNames.length, 2));
for (int i = 0; i < optionNames.length; i++) {
final String optionName = optionNames[i];
final JBTextField textField = myCustomOptions.get(optionName);
final GridConstraints labelConstraints = new GridConstraints();
labelConstraints.setFill(GridConstraints.FILL_VERTICAL);
labelConstraints.setRow(i);
labelConstraints.setColumn(0);
labelConstraints.setHSizePolicy(GridConstraints.SIZEPOLICY_CAN_SHRINK);
final JLabel label = new JLabel(StringUtil.capitalize(CAPITAL_LETTER.matcher(optionName).replaceAll(" ")));
label.setHorizontalAlignment(SwingConstants.LEFT);
myCustomOptionsPanel.add(label, labelConstraints);
final GridConstraints textConstraints = new GridConstraints();
textConstraints.setFill(GridConstraints.FILL_BOTH);
textConstraints.setRow(i);
textConstraints.setColumn(1);
textConstraints.setHSizePolicy(GridConstraints.SIZEPOLICY_CAN_GROW);
myCustomOptionsPanel.add(textField, textConstraints);
}
}
@NotNull
AbstractPyCommonOptionsForm getOptionsForm() {
return myOptionsForm;
}
@NotNull
public String getTarget() {
return myTargetText.getText();
}
public void setTarget(@NotNull final String targetText) {
myTargetText.setText(targetText);
}
private void configureElementsVisibility() {
final TestTargetType targetType = getTargetType();
myTargetText.setVisible(targetType != TestTargetType.CUSTOM);
myTargetText.getButton().setVisible(targetType == TestTargetType.FOLDER);
}
@SuppressWarnings("WeakerAccess") // Accessor for property
@NotNull
public TestTargetType getTargetType() {
return TestTargetType.valueOf(myButtonGroup.getSelection().getActionCommand());
}
@SuppressWarnings("unused") // Mutator for property
public void setTargetType(@NotNull final TestTargetType target) {
final Enumeration<AbstractButton> elements = myButtonGroup.getElements();
while (elements.hasMoreElements()) {
final AbstractButton button = elements.nextElement();
if (TestTargetType.valueOf(button.getActionCommand()) == target) {
myButtonGroup.setSelected(button.getModel(), true);
break;
}
}
configureElementsVisibility();
}
}
@@ -0,0 +1,404 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing.universalTests
import com.google.gson.Gson
import com.intellij.execution.Location
import com.intellij.execution.PsiLocation
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.execution.actions.ConfigurationFromContext
import com.intellij.execution.actions.RunConfigurationProducer
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.testframework.AbstractTestProxy
import com.intellij.execution.testframework.sm.runner.SMTestLocator
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.JDOMExternalizerUtil
import com.intellij.openapi.util.Pair
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.psi.PyClass
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyQualifiedNameOwner
import com.jetbrains.python.psi.impl.PyPsiFacadeImpl
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.run.CommandLinePatcher
import com.jetbrains.python.run.PythonConfigurationFactoryBase
import com.jetbrains.python.testing.*
import com.jetbrains.reflection.DelegationProperty
import com.jetbrains.reflection.Properties
import com.jetbrains.reflection.getProperties
import org.jdom.Element
import java.io.File
import java.util.*
import javax.swing.JComponent
/**
* New (universal) API for test runners.
*
* @author Ilya.Kazakevich
*/
fun isUniversalModeEnabled(): Boolean = Registry.`is`("python.tests.enableUniversalTests")
internal fun getAdditionalArgumentsPropertyName() = PyUniversalTestConfiguration::additionalArguments.name
/**
* Resolves qname of any symbol to appropriate PSI element.
*/
private fun findElementByQualifiedName(name: QualifiedName, module: Module, context: TypeEvalContext): PsiElement? {
val facade = PyPsiFacadeImpl.getInstance(module.project)
var currentName = name
var element: PsiElement? = null
// Drill as deep, as we can
var lastElement: String? = null
while (currentName.componentCount > 0 && element == null) {
element = facade.qualifiedNameResolver(currentName).fromModule(module).withMembers().firstResult()
if (element != null) {
break
}
lastElement = name.lastComponent!!
currentName = name.removeLastComponent()
}
if (lastElement != null && element is PyClass) {
// Drill in class
val method = element.findMethodByName(lastElement, true, context)
if (method != null) {
return method
}
}
return element
}
/**
* @return factory chosen by user in "test runner" settings
*/
private fun findConfigurationFactoryFromSettings(module: Module): ConfigurationFactory {
val name = TestRunnerService.getInstance(module).projectConfiguration
val factories = PyUniversalTestsConfigurationType.configurationFactories
val configurationFactory = factories.find { it.name == name }
return configurationFactory ?: factories.first()
}
private object PyUniversalTestsLocator : SMTestLocator {
override fun getLocation(protocol: String, path: String, project: Project, scope: GlobalSearchScope): MutableList<Location<PsiElement>> {
if (scope !is ModuleWithDependenciesScope) {
return ArrayList()
}
val result = findElementByQualifiedName(QualifiedName.fromDottedString(path), scope.module,
TypeEvalContext.userInitiated(project, null))
if (result != null) {
return arrayListOf(PsiLocation.fromPsiElement(result))
}
else {
return ArrayList()
}
}
}
abstract class PyUniversalTestExecutionEnvironment<T : PyUniversalTestConfiguration>(configuration: T, environment: ExecutionEnvironment)
: PythonTestCommandLineStateBase<T>(configuration, environment) {
override fun getTestLocator(): SMTestLocator = PyUniversalTestsLocator
override fun getTestSpecs(): MutableList<String> = ArrayList(configuration.getTestSpec())
override fun generateCommandLine(patchers: Array<out CommandLinePatcher>?): GeneralCommandLine {
val line = super.generateCommandLine(patchers)
line.workDirectory = File(configuration.workingDirectorySafe)
return line
}
}
abstract class PyUniversalTestSettingsEditor(private val form: PyUniversalTestForm)
: SettingsEditor<PyUniversalTestConfiguration>() {
override fun resetEditorFrom(s: PyUniversalTestConfiguration) {
// usePojoProperties is true because we know that Form is java-based
AbstractPythonRunConfiguration.copyParams(s, form.optionsForm)
s.copyTo(getProperties(form, usePojoProperties = true))
}
override fun applyEditorTo(s: PyUniversalTestConfiguration) {
AbstractPythonRunConfiguration.copyParams(form.optionsForm, s)
s.copyFrom(getProperties(form, usePojoProperties = true))
}
override fun createEditor(): JComponent = form.panel
}
enum class TestTargetType(val optionName: String) {
PYTHON("--target"), FOLDER("--path"), CUSTOM("")
}
/**
* Target depends on target type. It could be path to file/folder or python target
*/
data class ConfigurationTarget(@ConfigField var target: String, @ConfigField var targetType: TestTargetType) {
fun copyTo(dst: ConfigurationTarget) {
// TODO: do we have such method it in Kotlin?
dst.target = target
dst.targetType = targetType
}
}
/**
* Parent of all new test configurations.
* All config-specific fields are implemented as properties. They are saved/restored automatically and passed to GUI form.
*
* @param runBareFunctions if config supports running functions directly in modules or only class methods
*/
abstract class PyUniversalTestConfiguration(project: Project,
configurationFactory: ConfigurationFactory,
private val runBareFunctions: Boolean = true)
: AbstractPythonTestRunConfiguration<PyUniversalTestConfiguration>(project, configurationFactory), PyRerunAwareConfiguration {
@DelegationProperty
val target = ConfigurationTarget(".", TestTargetType.FOLDER)
@ConfigField
var additionalArguments = ""
val testFrameworkName = configurationFactory.name!!
private fun getTestSpecForPythonTarget(location: Location<*>): List<String> {
if (location !is PsiLocation) {
return emptyList()
}
if (location.psiElement !is PyQualifiedNameOwner) {
return emptyList()
}
val qualifiedName = (location.psiElement as PyQualifiedNameOwner).qualifiedName ?: return emptyList()
return listOf(TestTargetType.PYTHON.optionName, qualifiedName)
}
override fun getTestSpec(location: Location<*>, failedTest: AbstractTestProxy): String? {
val list = getTestSpecForPythonTarget(location)
if (list.isEmpty()) {
return null
}
else {
return list.joinToString(" ")
}
}
override fun getTestSpecsForRerun(scope: GlobalSearchScope,
locations: MutableList<Pair<Location<*>, AbstractTestProxy>>): List<String> {
val result = ArrayList<String>()
locations.map { getTestSpecForPythonTarget(it.first) }.filterNotNull().forEach { result.addAll(it) }
return result + generateRawArguments()
}
fun getTestSpec() = listOf(target.targetType.optionName, target.target) + generateRawArguments()
/**
* raw arguments to be added after "--" and passed to runner directly
*/
private fun generateRawArguments(): List<String> {
val rawArguments = additionalArguments + " " + getCustomRawArgumentsString()
if (rawArguments.isNotBlank()) {
return listOf("--") + rawArguments.trim().split(" ")
}
return emptyList()
}
/**
* @return configuration-specific arguments
*/
protected open fun getCustomRawArgumentsString() = ""
fun reset() {
target.target = "."
target.targetType = TestTargetType.FOLDER
additionalArguments = ""
}
fun copyFrom(src: Properties) {
src.copyTo(getConfigFields())
}
fun copyTo(dst: Properties) {
getConfigFields().copyTo(dst)
}
override fun writeExternal(element: Element) {
super.writeExternal(element)
val gson = Gson()
getConfigFields().properties.forEach {
JDOMExternalizerUtil.writeField(element, it.getName(), gson.toJson(it.get()))
}
}
override fun readExternal(element: Element) {
super.readExternal(element)
val gson = Gson()
getConfigFields().properties.forEach {
val fromJson: Any? = gson.fromJson(JDOMExternalizerUtil.readField(element, it.getName()), it.getType())
if (fromJson != null) {
it.set(fromJson)
}
}
}
private fun getConfigFields() = getProperties(this, ConfigField::class)
/**
* Checks if element could be test target for this config.
* Function is used to create tests by context.
*
* If yes, and element is [PsiElement] then it is [TestTargetType.PYTHON].
* If file then [TestTargetType.FOLDER]
*/
fun couldBeTestTarget(element: PsiElement) =
// TODO: PythonUnitTestUtil logic is weak. We should give user ability to launch test on symbol since user knows better if folder
// contains tests etc
when (element) {
is PyFile -> PythonUnitTestUtil.isUnitTestFile(element)
is PyFunction -> PythonUnitTestUtil.isTestCaseFunction(element, runBareFunctions)
is PyClass -> PythonUnitTestUtil.isTestCaseClass(element, TypeEvalContext.userInitiated(element.project, element.containingFile))
else -> false
}
}
object PyUniversalTestsConfigurationType : PythonTestConfigurationType() {
override fun getId() = "py_universal_tests"
override fun getConfigurationFactories(): Array<ConfigurationFactory> {
if (isUniversalModeEnabled()) {
return arrayOf(PyUniversalUnitTestFactory,
PyUniversalPyTestFactory,
PyUniversalNoseTestFactory)
}
// Array can't be empty according to contract (type is fetched from first element)
return arrayOf(PyUniversalUnitTestFactory)
}
}
abstract class PyUniversalTestFactory : PythonConfigurationFactoryBase(PyUniversalTestsConfigurationType)
/**
* Only one producer is registered with EP, but it uses factory configured by user to prdouce different configs
*/
object PyUniversalTestsConfigurationProducer : RunConfigurationProducer<PyUniversalTestConfiguration>(PyUniversalTestsConfigurationType) {
override fun cloneTemplateConfiguration(context: ConfigurationContext): RunnerAndConfigurationSettings {
return cloneTemplateConfigurationStatic(context, findConfigurationFactoryFromSettings(context.module))
}
override fun findOrCreateConfigurationFromContext(context: ConfigurationContext?): ConfigurationFromContext? {
if (!isUniversalModeEnabled()) {
return null
}
return super.findOrCreateConfigurationFromContext(context)
}
override fun setupConfigurationFromContext(configuration: PyUniversalTestConfiguration?,
context: ConfigurationContext?,
sourceElement: Ref<PsiElement>?): Boolean {
if (sourceElement == null || configuration == null) {
return false
}
val nameAndTarget = getNameAndTargetForConfig(configuration, sourceElement.get()) ?: return false
configuration.name = nameAndTarget.first
nameAndTarget.second.copyTo(configuration.target)
return true
}
// TODO: DOC
private fun getNameAndTargetForConfig(configuration: PyUniversalTestConfiguration,
baseElement: PsiElement): Pair<String, ConfigurationTarget>? {
var element = baseElement
// Go up until we reach top of the file
// asking configuration about each element if it is supported or not
// If element is supported -- set it as configuration target
do {
if (configuration.couldBeTestTarget(element)) {
when (element) {
is PyQualifiedNameOwner -> { // Function, class, method
val qualifiedName = element.qualifiedName
if (qualifiedName == null) {
Logger.getInstance(PyUniversalTestConfiguration::class.java).warn("$element has no qualified name")
return null
}
return Pair(qualifiedName, ConfigurationTarget(qualifiedName, TestTargetType.PYTHON))
}
is PsiFile -> return Pair(element.virtualFile.name, ConfigurationTarget(element.virtualFile.path, TestTargetType.FOLDER))
}
}
element = element.parent
}
while (element !is PsiDirectory) // if parent is folder, then we are at file level
return null
}
override fun isConfigurationFromContext(configuration: PyUniversalTestConfiguration?, context: ConfigurationContext?): Boolean {
val psiElement = context?.psiLocation ?: return false
val nameAndTarget = getNameAndTargetForConfig(configuration!!, psiElement) ?: return false
return configuration.target == nameAndTarget.second
}
override fun isPreferredConfiguration(self: ConfigurationFromContext?, other: ConfigurationFromContext?): Boolean {
if (self == null || other == null) {
return false
}
val module = ModuleUtil.findModuleForPsiElement(self.sourceElement) ?: return false
return self.configuration.factory == findConfigurationFactoryFromSettings(module)
}
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
/**
* Mark run configuration field with it to enable saving, resotring and form iteraction
*/
annotation class ConfigField
@@ -0,0 +1,55 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.testing.universalTests
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.testing.PythonTestConfigurationsModel
/**
* unittest
*/
class PyUniversalUnitTestSettingsEditor(configuration: PyUniversalTestConfiguration) :
PyUniversalTestSettingsEditor(PyUniversalTestForm.create(configuration))
class PyUniversalUnitTestExecutionEnvironment(configuration: PyUniversalUnitTestConfiguration, environment: ExecutionEnvironment) :
PyUniversalTestExecutionEnvironment<PyUniversalUnitTestConfiguration>(configuration, environment) {
override fun getRunner() = PythonHelper.UNIVERSAL_UT
}
class PyUniversalUnitTestConfiguration(project: Project, factory: PyUniversalTestFactory) :
PyUniversalTestConfiguration(project, factory, runBareFunctions = false) { // Bare functions not supported in unittest: classes only
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? =
PyUniversalUnitTestExecutionEnvironment(this, environment)
override fun createConfigurationEditor(): SettingsEditor<PyUniversalTestConfiguration> =
PyUniversalUnitTestSettingsEditor(this)
}
object PyUniversalUnitTestFactory : PyUniversalTestFactory() {
override fun createTemplateConfiguration(project: Project) = PyUniversalUnitTestConfiguration(project, this)
override fun getName(): String = PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME
}
@@ -0,0 +1,21 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* New (universal) test runners for Python based on this
* <a href="https://confluence.jetbrains.com/display/~link/PyCharm+test+runners+protocol">Protocol</a>.
*/
package com.jetbrains.python.testing.universalTests;
@@ -0,0 +1,155 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.reflection
import com.intellij.util.containers.HashMap
import java.beans.Introspector
import java.beans.PropertyDescriptor
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.javaType
import kotlin.reflect.memberProperties
/**
* Tools to fetch properties both from Java and Kotlin code and to copy them from one object to another.
* To be a property container class should have kotlin properties, java bean properties or implement [SimplePropertiesProvider].
*
* Properties should be writable except [DelegationProperty]
* @author Ilya.Kazakevich
*/
interface Property {
fun getName(): String
fun getType(): java.lang.reflect.Type
fun get(): Any?
fun set(value: Any?)
}
private class KotlinProperty(val property: KMutableProperty<*>, val instance: Any?) : Property {
override fun getName() = property.name
override fun getType() = property.returnType.javaType
override fun get() = property.getter.call(instance)
override fun set(value: Any?) = property.setter.call(instance, value)
}
private class JavaProperty(val property: PropertyDescriptor, val instance: Any?) : Property {
override fun getName() = property.name!!
override fun getType() = property.propertyType!!
override fun get() = property.readMethod.invoke(instance)!!
override fun set(value: Any?) {
property.writeMethod.invoke(instance, value)
}
}
private class SimpleProperty(private val propertyName: String,
private val provider: SimplePropertiesProvider) : Property {
override fun getName() = propertyName
override fun getType() = String::class.java
override fun get() = provider.getPropertyValue(propertyName)
override fun set(value: Any?) = provider.setPropertyValue(propertyName, value?.toString())
}
/**
* Implement to handle properties manually
*/
interface SimplePropertiesProvider {
val propertyNames: List<String>
fun setPropertyValue(propertyName: String, propertyValue: String?)
fun getPropertyValue(propertyName: String): String?
}
class Properties(val properties: List<Property>, val instance: Any) {
val propertiesMap: MutableMap<String, Property> = HashMap(properties.map { Pair(it.getName(), it) }.toMap())
init {
if (instance is SimplePropertiesProvider) {
instance.propertyNames.forEach { propertiesMap.put(it, SimpleProperty(it, instance)) }
}
}
fun copyTo(dst: Properties) {
propertiesMap.values.forEach {
val dstProperty = dst.propertiesMap[it.getName()]
if (dstProperty != null) {
val value = it.get()
dstProperty.set(value)
}
}
}
}
private fun KProperty<*>.isAnnotated(annotation: KClass<*>): Boolean {
return this.annotations.find { annotation.java.isAssignableFrom(it.javaClass) } != null
}
/**
* @param instance object with properties (see module doc)
* @param annotationToFilterBy optional annotation class to fetch only kotlin properties annotated with it. Only supported in Kotlin
* @param usePojoProperties search for java-style properties (kotlin otherwise)
* @return properties of some object
*/
fun getProperties(instance: Any, annotationToFilterBy: KClass<*>? = null, usePojoProperties: Boolean = false): Properties {
if (usePojoProperties) {
// Java props
val javaProperties = Introspector.getBeanInfo(instance.javaClass).propertyDescriptors
assert(annotationToFilterBy == null, { "Filtering java properties is not supported" })
return Properties(javaProperties.map { JavaProperty(it, instance) }, instance)
}
else {
// Kotlin props
val klass = instance.javaClass.kotlin
val allKotlinProperties = LinkedHashSet(klass.memberProperties.filterIsInstance(KProperty::class.java))
var delegatedProperties = listOf<Property>() // See DelegationProperty doc
allKotlinProperties.filter { it.isAnnotated(DelegationProperty::class) }.forEach {
val delegatedInstance = it.getter.call(instance)
if (delegatedInstance != null) {
delegatedProperties = getProperties(delegatedInstance, annotationToFilterBy, false).properties
allKotlinProperties.remove(it)
}
}
val firstLevelProperties = allKotlinProperties.filterIsInstance(KMutableProperty::class.java)
if (annotationToFilterBy == null) {
return Properties(firstLevelProperties.map { KotlinProperty(it, instance) } + delegatedProperties, instance)
}
return Properties(
firstLevelProperties.filter { it.isAnnotated(annotationToFilterBy) }.map { KotlinProperty(it, instance) } + delegatedProperties, instance)
}
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
/**
* Property marked with it is not considered to be [Property] by itself, but class with properties instead.
* Following structure is example:
* class User:
* +familyName: String
* +lastName: String
* +credentials: Credentials
*
* class Credentials:
* +login: String
* +password: String
*
* Property credentials here is [DelegationProperty]. It can be val, but all other properties should be var
*/
annotation class DelegationProperty
@@ -23,7 +23,7 @@ import com.intellij.psi.PsiElement;
import com.jetbrains.env.PyExecutionFixtureTestTask;
import com.jetbrains.python.sdk.InvalidSdkException;
import com.jetbrains.python.sdkTools.SdkCreationType;
import com.jetbrains.python.testing.PythonTestConfigurationProducer;
import com.jetbrains.python.testing.PythonTestOldConfigurationProducer;
import com.jetbrains.python.testing.TestRunnerService;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
@@ -43,13 +43,13 @@ class CreateConfigurationTestTask extends PyExecutionFixtureTestTask {
@NotNull
private final String myTestRunnerName;
@NotNull
private final Class<? extends PythonTestConfigurationProducer> myProducer;
private final Class<? extends PythonTestOldConfigurationProducer> myProducer;
/**
* @param producer class of configuration producer to check
* @param testRunnerName test runner name (to set as default to make sure producer launched)
*/
CreateConfigurationTestTask(@NotNull final Class<? extends PythonTestConfigurationProducer> producer,
CreateConfigurationTestTask(@NotNull final Class<? extends PythonTestOldConfigurationProducer> producer,
@NotNull final String testRunnerName) {
super("/testRunner/env/createConfigurationTest/");
myProducer = producer;
@@ -74,7 +74,7 @@ class CreateConfigurationTestTask extends PyExecutionFixtureTestTask {
private void checkConfigurationCreatedFrom(@NotNull final PsiElement element) {
final PythonTestConfigurationProducer producer = createProducer();
final PythonTestOldConfigurationProducer producer = createProducer();
final ConfigurationFromContext context =
producer.createConfigurationFromContext(new ConfigurationContext(element));
Assert.assertNotNull(String.format("Failed to create context for %s", myTestRunnerName), context);
@@ -82,7 +82,7 @@ class CreateConfigurationTestTask extends PyExecutionFixtureTestTask {
}
@NotNull
private PythonTestConfigurationProducer createProducer() {
private PythonTestOldConfigurationProducer createProducer() {
try {
return myProducer.getConstructor().newInstance();
}
@@ -16,7 +16,7 @@
package com.jetbrains.env.ut;
import com.jetbrains.env.ProcessWithConsoleRunner;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import com.jetbrains.python.testing.doctest.PythonDocTestRunConfiguration;
import org.jetbrains.annotations.NotNull;
@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
*/
public class PyDocTestProcessRunner extends PyScriptTestProcessRunner<PythonDocTestRunConfiguration> {
public PyDocTestProcessRunner(@NotNull final String scriptName, final int timesToRerunFailedTests) {
super(PythonTestConfigurationType.getInstance().PY_DOCTEST_FACTORY,
super(PythonTestOldConfigurationType.getInstance().PY_DOCTEST_FACTORY,
PythonDocTestRunConfiguration.class, scriptName, timesToRerunFailedTests);
}
}
@@ -16,7 +16,7 @@
package com.jetbrains.env.ut;
import com.jetbrains.env.ProcessWithConsoleRunner;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import com.jetbrains.python.testing.nosetest.PythonNoseTestRunConfiguration;
import org.jetbrains.annotations.NotNull;
@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
*/
public class PyNoseTestProcessRunner extends PyScriptTestProcessRunner<PythonNoseTestRunConfiguration> {
public PyNoseTestProcessRunner(@NotNull final String scriptName, final int timesToRerunFailedTests) {
super(PythonTestConfigurationType.getInstance().PY_NOSETEST_FACTORY,
super(PythonTestOldConfigurationType.getInstance().PY_NOSETEST_FACTORY,
PythonNoseTestRunConfiguration.class, scriptName, timesToRerunFailedTests);
}
}
@@ -16,7 +16,7 @@
package com.jetbrains.env.ut;
import com.jetbrains.env.ProcessWithConsoleRunner;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import com.jetbrains.python.testing.pytest.PyTestRunConfiguration;
import org.jetbrains.annotations.NotNull;
@@ -29,7 +29,7 @@ import java.io.IOException;
*/
public class PyTestTestProcessRunner extends PyScriptTestProcessRunner<PyTestRunConfiguration> {
public PyTestTestProcessRunner(@NotNull final String scriptName, final int timesToRerunFailedTests) {
super(PythonTestConfigurationType.getInstance().PY_PYTEST_FACTORY,
super(PythonTestOldConfigurationType.getInstance().PY_PYTEST_FACTORY,
PyTestRunConfiguration.class, scriptName, timesToRerunFailedTests);
}
@@ -16,7 +16,7 @@
package com.jetbrains.env.ut;
import com.jetbrains.env.ProcessWithConsoleRunner;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import com.jetbrains.python.testing.unittest.PythonUnitTestRunConfiguration;
import org.jetbrains.annotations.NotNull;
@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
*/
public class PyUnitTestProcessRunner extends PyScriptTestProcessRunner<PythonUnitTestRunConfiguration> {
public PyUnitTestProcessRunner(@NotNull final String scriptName, final int timesToRerunFailedTests) {
super(PythonTestConfigurationType.getInstance().PY_UNITTEST_FACTORY,
super(PythonTestOldConfigurationType.getInstance().PY_UNITTEST_FACTORY,
PythonUnitTestRunConfiguration.class, scriptName, timesToRerunFailedTests);
}
}
+5 -5
View File
@@ -40,8 +40,8 @@ import com.jetbrains.python.PyNames;
import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.flavors.JythonSdkFlavor;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.AbstractPythonOldTestRunConfiguration;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
@@ -154,7 +154,7 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask {
@Override
public void runTestOn(String sdkHome) throws Exception {
final Project project = getProject();
final ConfigurationFactory factory = PythonTestConfigurationType.getInstance().PY_UNITTEST_FACTORY;
final ConfigurationFactory factory = PythonTestOldConfigurationType.getInstance().PY_UNITTEST_FACTORY;
runConfiguration(factory, sdkHome, project);
}
@@ -162,7 +162,7 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask {
final RunnerAndConfigurationSettings settings =
RunManager.getInstance(project).createRunConfiguration("test", factory);
AbstractPythonTestRunConfiguration config = (AbstractPythonTestRunConfiguration)settings.getConfiguration();
AbstractPythonOldTestRunConfiguration config = (AbstractPythonOldTestRunConfiguration)settings.getConfiguration();
config.setSdkHome(sdkHome);
@@ -271,7 +271,7 @@ public abstract class PyUnitTestTask extends PyExecutionFixtureTestTask {
return 60000;
}
protected void configure(AbstractPythonTestRunConfiguration config) {
protected void configure(AbstractPythonOldTestRunConfiguration config) {
}
/**
@@ -18,7 +18,7 @@ package com.jetbrains.python;
import com.jetbrains.python.fixtures.PyCommandLineTestCase;
import com.jetbrains.python.run.PythonConfigurationType;
import com.jetbrains.python.run.PythonRunConfiguration;
import com.jetbrains.python.testing.PythonTestConfigurationType;
import com.jetbrains.python.testing.PythonTestOldConfigurationType;
import com.jetbrains.python.testing.unittest.PythonUnitTestRunConfiguration;
import junit.framework.Assert;
@@ -31,7 +31,7 @@ public class PythonRunConfigurationTest extends PyCommandLineTestCase {
private static final String PY_SCRIPT = "foo.py";
public void testUnitTestCommandLine() {
PythonUnitTestRunConfiguration configuration = createConfiguration(PythonTestConfigurationType.getInstance(),
PythonUnitTestRunConfiguration configuration = createConfiguration(PythonTestOldConfigurationType.getInstance(),
PythonUnitTestRunConfiguration.class);
configuration.setScriptName(PY_SCRIPT);
final List<String> params = buildRunCommandLine(configuration);