mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
@@ -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
|
||||
@@ -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')
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
+29
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -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";
|
||||
|
||||
+3
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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;
|
||||
|
||||
+4
-4
@@ -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()));
|
||||
}
|
||||
|
||||
+4
-4
@@ -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 &&
|
||||
|
||||
+2
-3
@@ -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";
|
||||
|
||||
+2
-2
@@ -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
|
||||
+5
-5
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user