Merge branch 'master' into PY-9727
authorIlya.Kazakevich <Ilya.Kazakevich@jetbrains.com>
Wed, 9 Dec 2015 20:20:27 +0000 (23:20 +0300)
committerIlya.Kazakevich <Ilya.Kazakevich@jetbrains.com>
Wed, 9 Dec 2015 20:20:27 +0000 (23:20 +0300)
25 files changed:
python/helpers/pycharm/_jb_tox_runner.py [new file with mode: 0644]
python/helpers/pycharm/tcmessages.py
python/helpers/pycharm/tcunittest.py
python/helpers/pycharm/utrunner.py
python/src/META-INF/python-core.xml
python/src/com/jetbrains/python/PythonHelper.java
python/src/com/jetbrains/python/run/AbstractPythonRunConfiguration.java
python/src/com/jetbrains/python/testing/PyRerunFailedTestsAction.java
python/src/com/jetbrains/python/testing/PythonTestCommandLineStateBase.java
python/src/com/jetbrains/python/testing/PythonTestConfigurationProducer.java
python/src/com/jetbrains/python/testing/attest/PythonAtTestCommandLineState.java
python/src/com/jetbrains/python/testing/doctest/PythonDocTestCommandLineState.java
python/src/com/jetbrains/python/testing/nosetest/PythonNoseTestCommandLineState.java
python/src/com/jetbrains/python/testing/pytest/PyTestCommandLineState.java
python/src/com/jetbrains/python/testing/tox/PyToxCommandLineState.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/PyToxConfiguration.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/PyToxConfigurationFactory.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/PyToxConfigurationProducer.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/PyToxConfigurationSettings.form [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/PyToxConfigurationSettings.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/PyToxConfigurationType.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/tox/package-info.java [new file with mode: 0644]
python/src/com/jetbrains/python/testing/unittest/PythonUnitTestCommandLineState.java
python/src/com/jetbrains/serialization/AnnotationSerializationFilter.java [new file with mode: 0644]
python/src/com/jetbrains/serialization/CompoundFilter.java [new file with mode: 0644]

diff --git a/python/helpers/pycharm/_jb_tox_runner.py b/python/helpers/pycharm/_jb_tox_runner.py
new file mode 100644 (file)
index 0000000..6a297cf
--- /dev/null
@@ -0,0 +1,93 @@
+# coding=utf-8
+"""
+Runs tox from current directory.
+It supports any runner, but well-known runners (py.test and unittest) are switched to our internal runners to provide
+better support
+"""
+import os
+import sys
+
+from tox import config as tox_config, session as tox_session
+from tox.session import Reporter
+
+from tcmessages import TeamcityServiceMessages
+
+helpers_dir = str(os.path.split(__file__)[0])
+
+
+class _Unit2(object):
+    def fix(self, command):
+        if command[0] != "unit2":
+            return None
+        return [os.path.join(helpers_dir, "utrunner.py"), os.getcwd()] + command[1:] + ["true"]
+
+
+class _PyTest(object):
+    def fix(self, command):
+        if command[0] != "py.test":
+            return None
+        return [os.path.join(helpers_dir, "pytestrunner.py"), "-p", "pytest_teamcity", os.getcwd()] + command[1:]
+
+
+class _Nose(object):
+    def fix(self, command):
+        if command[0] != "nosetests":
+            return None
+        return [os.path.join(helpers_dir, "noserunner.py"), os.getcwd()] + command[1:]
+
+
+
+_RUNNERS = [_Unit2(), _PyTest(), _Nose()]
+
+teamcity = TeamcityServiceMessages()
+
+
+class _Reporter(Reporter):
+    def logaction_start(self, action):
+        super(_Reporter, self).logaction_start(action)
+        if action.activity == "getenv":
+            teamcity.testSuiteStarted(action.id)
+            self.current_suite = action.id
+
+    def logaction_finish(self, action):
+        super(_Reporter, self).logaction_finish(action)
+        if action.activity == "runtests":
+            teamcity.testSuiteFinished(action.id)
+
+    def error(self, msg):
+        super(_Reporter, self).error(msg)
+        name = teamcity.current_test_name()
+        if name:
+            if name != teamcity.topmost_suite:
+                teamcity.testFailed(name, msg)
+            else:
+                teamcity.testFailed("ERROR", msg)
+                teamcity.testSuiteFinished(name)
+        else:
+            sys.stderr.write(msg)
+
+    def skip(self, msg):
+        super(_Reporter, self).skip(msg)
+        name = teamcity.current_test_name()
+        if name:
+            teamcity.testFinished(name)
+
+
+config = tox_config.parseconfig()
+for env, tmp_config in config.envconfigs.items():
+    if not tmp_config.setenv:
+        tmp_config.setenv = dict()
+    tmp_config.setenv.update({"_jb_do_not_call_enter_matrix": "1"})
+    commands = tmp_config.commands
+    if not isinstance(commands, list) or not len(commands):
+        continue
+    for fixer in _RUNNERS:
+        for i, command in enumerate(commands):
+            fixed_command = fixer.fix(command)
+            if fixed_command:
+                commands[i] = fixed_command
+    tmp_config.commands = commands
+
+session = tox_session.Session(config, Report=_Reporter)
+teamcity.testMatrixEntered()
+session.runcommand()
index a567ed5438e5136c4094fe228f62b754a9c2cc8c..d9601da2129cd5f5f458690efca29c27a1625c3f 100644 (file)
@@ -7,6 +7,14 @@ class TeamcityServiceMessages:
     def __init__(self, output=sys.stdout, prepend_linebreak=False):
         self.output = output
         self.prepend_linebreak = prepend_linebreak
+        self.test_stack = []
+        """
+        Names of tests
+        """
+        self.topmost_suite = None
+        """
+        Last suite we entered in
+        """
 
     def escapeValue(self, value):
         if sys.version_info[0] <= 2 and isinstance(value, unicode):
@@ -28,21 +36,27 @@ class TeamcityServiceMessages:
 
     def testSuiteStarted(self, suiteName, location=None):
         self.message('testSuiteStarted', name=suiteName, locationHint=location)
+        self.test_stack.append(suiteName)
+        self.topmost_suite = suiteName
 
     def testSuiteFinished(self, suiteName):
         self.message('testSuiteFinished', name=suiteName)
+        self.__pop_current_test()
 
     def testStarted(self, testName, location=None):
         self.message('testStarted', name=testName, locationHint=location)
+        self.test_stack.append(testName)
+
 
     def testFinished(self, testName, duration=None):
         self.message('testFinished', name=testName, duration=duration)
+        self.__pop_current_test()
+
 
     def testIgnored(self, testName, message=''):
         self.message('testIgnored', name=testName, message=message)
         self.testFinished(testName)
 
-
     def testFailed(self, testName, message='', details='', expected='', actual='', duration=None):
         """
         Marks test as failed. *CAUTION*: This method calls ``testFinished``, so you do not need
@@ -56,10 +70,24 @@ class TeamcityServiceMessages:
             self.message('testFailed', name=testName, message=message, details=details)
         self.testFinished(testName, int(duration) if duration else None)
 
+
+    def __pop_current_test(self):
+        try:
+            self.test_stack.pop()
+        except IndexError:
+            pass
+
     def testError(self, testName, message='', details='', duration=None):
         self.message('testFailed', name=testName, message=message, details=details, error="true")
         self.testFinished(testName, int(duration) if duration else None)
 
+
+    def current_test_name(self):
+        """
+        :return: name of current test we are in
+        """
+        return self.test_stack[-1] if len(self.test_stack) > 0 else None
+
     def testStdOut(self, testName, out):
         self.message('testStdOut', name=testName, out=out)
 
index b688d804c653e1c07b069fda31a3cae7fb1c5ccb..185c12898a1e948c59e310337cc76468f85df011 100644 (file)
@@ -1,3 +1,4 @@
+import os
 import traceback, sys
 from unittest import TestResult
 import datetime
@@ -37,13 +38,18 @@ def smart_str(s):
 
 
 class TeamcityTestResult(TestResult):
+  """
+  Set ``_jb_do_not_call_enter_matrix`` to prevent it from runnig "enter matrix"
+  """
+
   def __init__(self, stream=sys.stdout, *args, **kwargs):
     TestResult.__init__(self)
     for arg, value in kwargs.items():
       setattr(self, arg, value)
     self.output = stream
     self.messages = TeamcityServiceMessages(self.output, prepend_linebreak=True)
-    self.messages.testMatrixEntered()
+    if not "_jb_do_not_call_enter_matrix" in os.environ:
+      self.messages.testMatrixEntered()
     self.current_failed = False
     self.current_suite = None
     self.subtest_suite = None
index bd04fedcff5376df9ae28317bd0462d29b738088..dfaaad58e12ad383376ac7463a72ef003622b7a0 100644 (file)
@@ -38,6 +38,8 @@ def loadSource(fileName):
       cnt += 1
     moduleName = getModuleName(prefix, cnt)
   debug("/ Loading " + fileName + " as " + moduleName)
+  if os.path.isdir(fileName):
+    fileName = fileName + os.path.sep
   module = imp.load_source(moduleName, fileName)
   modules[moduleName] = module
   return module
@@ -56,17 +58,15 @@ def walkModules(modulesAndPattern, dirname, names):
 # For default pattern see https://docs.python.org/2/library/unittest.html#test-discovery
 def loadModulesFromFolderRec(folder, pattern="test*.py"):
   modules = []
-  if PYTHON_VERSION_MAJOR == 3:
-    # fnmatch converts glob to regexp
-    prog_list = [re.compile(fnmatch.translate(pat.strip())) for pat in pattern.split(',')]
-    for root, dirs, files in os.walk(folder):
-      for name in files:
-        for prog in prog_list:
-          if name.endswith(".py") and prog.match(name):
-            modules.append(loadSource(os.path.join(root, name)))
-  else:   # actually for jython compatibility
-    os.path.walk(folder, walkModules, (modules, pattern))
-
+  # fnmatch converts glob to regexp
+  prog_list = [re.compile(fnmatch.translate(pat.strip())) for pat in pattern.split(',')]
+  for root, dirs, files in os.walk(folder):
+    files = [f for f in files if not f[0] == '.']
+    dirs[:] = [d for d in dirs if not d[0] == '.']
+    for name in files:
+      for prog in prog_list:
+        if name.endswith(".py") and prog.match(name):
+          modules.append(loadSource(os.path.join(root, name)))
   return modules
 
 testLoader = TestLoader()
@@ -109,11 +109,11 @@ if __name__ == "__main__":
       a_splitted = a[0].split("_args_separator_")  # ";" can't be used with bash, so we use "_args_separator_"
       if len(a_splitted) != 1:
         # means we have pattern to match against
-        if a_splitted[0].endswith(os.path.sep):
+        if os.path.isdir(a_splitted[0]):
           debug("/ from folder " + a_splitted[0] + ". Use pattern: " + a_splitted[1])
           modules = loadModulesFromFolderRec(a_splitted[0], a_splitted[1])
       else:
-        if a[0].endswith(os.path.sep):
+        if  os.path.isdir(a[0]):
           debug("/ from folder " + a[0])
           modules = loadModulesFromFolderRec(a[0])
         else:
index 72222837670b59f73240555cef95071b2ddc60a0..6b5c77424663bac9b5c705dfb500458e42acd6f8 100644 (file)
     <xdebugger.breakpointType implementation="com.jetbrains.python.debugger.PyExceptionBreakpointType"/>
 
     <configurationType implementation="com.jetbrains.python.testing.PythonTestConfigurationType"/>
+    <configurationType implementation="com.jetbrains.python.testing.tox.PyToxConfigurationType"/>
 
     <runConfigurationProducer implementation="com.jetbrains.python.testing.unittest.PythonUnitTestConfigurationProducer"/>
     <runConfigurationProducer implementation="com.jetbrains.python.testing.pytest.PyTestConfigurationProducer"/>
     <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.attest.PythonAtTestConfigurationProducer"/>
 
index 3816e5ba9f04f6f2cfa138a0300ced7f7b55380d..1eee40c98919a75bd29d647124936b9bd385e986 100644 (file)
@@ -50,6 +50,7 @@ public enum PythonHelper implements HelperPackage {
 
   // Test runners
   UT("pycharm", "utrunner"),
+  TOX("pycharm", "_jb_tox_runner"),
   SETUPPY("pycharm", "pycharm_setup_runner"),
   NOSE("pycharm", "noserunner"),
   PYTEST("pycharm", "pytestrunner"),
index f52d6c8fad509a7ee286b91e8c5d9beba29df43e..753c5616dcdce4ff827b7556c30468bdf31e5c3a 100644 (file)
@@ -73,6 +73,11 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractRunConfig
   private boolean myAddSourceRoots = true;
 
   protected PathMappingSettings myMappingSettings;
+  /**
+   * To prevent "double module saving" child may enable this flag
+   * and no module info would be saved
+   */
+  protected boolean mySkipModuleSerialization;
 
   public AbstractPythonRunConfiguration(Project project, final ConfigurationFactory factory) {
     super(project, factory);
@@ -235,7 +240,9 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractRunConfig
     myAddContentRoots = addContentRoots == null || Boolean.parseBoolean(addContentRoots);
     final String addSourceRoots = JDOMExternalizerUtil.readField(element, "ADD_SOURCE_ROOTS");
     myAddSourceRoots = addSourceRoots == null || Boolean.parseBoolean(addSourceRoots);
-    getConfigurationModule().readExternal(element);
+    if ( !mySkipModuleSerialization) {
+      getConfigurationModule().readExternal(element);
+    }
 
     setMappingSettings(PathMappingSettings.readExternal(element));
     // extension settings:
@@ -259,7 +266,9 @@ public abstract class AbstractPythonRunConfiguration<T extends AbstractRunConfig
     JDOMExternalizerUtil.writeField(element, "IS_MODULE_SDK", Boolean.toString(myUseModuleSdk));
     JDOMExternalizerUtil.writeField(element, "ADD_CONTENT_ROOTS", Boolean.toString(myAddContentRoots));
     JDOMExternalizerUtil.writeField(element, "ADD_SOURCE_ROOTS", Boolean.toString(myAddSourceRoots));
-    getConfigurationModule().writeExternal(element);
+    if ( !mySkipModuleSerialization) {
+      getConfigurationModule().writeExternal(element);
+    }
 
     // extension settings:
     PythonRunConfigurationExtensionsManager.getInstance().writeExternal(this, element);
index 5beca8a17bae645d27aefb192ee7a81e957603df..8dea7e5b247996c7a624a5c096ff9f505934c3a5 100644 (file)
@@ -104,6 +104,7 @@ public class PyRerunFailedTestsAction extends AbstractRerunFailedTestsAction {
       return myState.getRunner();
     }
 
+    @NotNull
     @Override
     protected List<String> getTestSpecs() {
       List<String> specs = new ArrayList<String>();
index b702b94b77d629378adda029544b72b0b8f0d058..103f2a0856ec7c54f1985396fb73364c385772df 100644 (file)
@@ -173,5 +173,6 @@ public abstract class PythonTestCommandLineStateBase extends PythonCommandLineSt
   }
 
   protected abstract HelperPackage getRunner();
+  @NotNull
   protected abstract List<String> getTestSpecs();
 }
index 10f7e018d1fc1bfb89fd15a750a5922f95a609a7..21ab8076ab84a2bb90cfb9971f45e9f4f1bae622 100644 (file)
@@ -17,6 +17,7 @@ package com.jetbrains.python.testing;
 
 import com.google.common.collect.Sets;
 import com.intellij.execution.Location;
+import com.intellij.execution.RunnerAndConfigurationSettings;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.actions.ConfigurationFromContext;
 import com.intellij.execution.actions.RunConfigurationProducer;
@@ -58,7 +59,7 @@ abstract public class PythonTestConfigurationProducer extends RunConfigurationPr
     super(configurationFactory);
   }
 
-  @Override
+    @Override
   public boolean isConfigurationFromContext(AbstractPythonTestRunConfiguration configuration, ConfigurationContext context) {
     final Location location = context.getLocation();
     if (location == null || !isAvailable(location)) return false;
index cfe65d3067a19881beb5b28fcca050b1a0a8f78f..c0c0a93e84857f0a047034edfc12bcaf2273c484 100644 (file)
@@ -19,6 +19,7 @@ import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.openapi.util.io.FileUtil;
 import com.jetbrains.python.PythonHelper;
 import com.jetbrains.python.testing.PythonTestCommandLineStateBase;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -40,6 +41,7 @@ public class PythonAtTestCommandLineState extends PythonTestCommandLineStateBase
     return PythonHelper.ATTEST;
   }
 
+  @NotNull
   protected List<String> getTestSpecs() {
     List<String> specs = new ArrayList<String>();
 
index a47242aad107f7f955a09b39583f1ad99da65860..e12e93d3edabed7b15562107c461de673d9a1e51 100644 (file)
@@ -18,6 +18,7 @@ package com.jetbrains.python.testing.doctest;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.jetbrains.python.PythonHelper;
 import com.jetbrains.python.testing.PythonTestCommandLineStateBase;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -39,6 +40,7 @@ public class PythonDocTestCommandLineState extends PythonTestCommandLineStateBas
     return PythonHelper.DOCSTRING;
   }
 
+  @NotNull
   protected List<String> getTestSpecs() {
     List<String> specs = new ArrayList<String>();
 
index 4d9393b8cedd7d94370d8b6868e7226d0b2d3a74..0dc5647eac1640c037701b6842c597445a704ce0 100644 (file)
@@ -22,6 +22,7 @@ import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.text.StringUtil;
 import com.jetbrains.python.PythonHelper;
 import com.jetbrains.python.testing.PythonTestCommandLineStateBase;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,6 +43,7 @@ public class PythonNoseTestCommandLineState extends PythonTestCommandLineStateBa
     return PythonHelper.NOSE;
   }
 
+  @NotNull
   protected List<String> getTestSpecs() {
     List<String> specs = new ArrayList<String>();
 
index dd202187e83c52aeefa2d01bb2489b183af7daa7..d44a935085541edfb053f7da285b5956b27bd3a5 100644 (file)
@@ -55,6 +55,7 @@ public class PyTestCommandLineState extends PythonTestCommandLineStateBase {
     return PythonHelper.PYTEST;
   }
 
+  @NotNull
   @Override
   protected List<String> getTestSpecs() {
     List<String> specs = new ArrayList<String>();
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxCommandLineState.java b/python/src/com/jetbrains/python/testing/tox/PyToxCommandLineState.java
new file mode 100644 (file)
index 0000000..81ee296
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2015 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.tox;
+
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.configurations.ParamsGroup;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.jetbrains.python.HelperPackage;
+import com.jetbrains.python.PythonHelper;
+import com.jetbrains.python.testing.PythonTestCommandLineStateBase;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Ilya.Kazakevich
+ */
+class PyToxCommandLineState extends PythonTestCommandLineStateBase {
+
+  @NotNull
+  private final String[] myArguments;
+
+
+  PyToxCommandLineState(@NotNull final PyToxConfiguration configuration,
+                                @NotNull final ExecutionEnvironment environment,
+                                @NotNull final String... arguments) {
+    super(configuration, environment);
+    myArguments = arguments;
+  }
+
+  @Override
+  protected HelperPackage getRunner() {
+    return PythonHelper.TOX;
+  }
+
+
+  @Override
+  public GeneralCommandLine generateCommandLine() {
+    final GeneralCommandLine line = super.generateCommandLine();
+    final ParamsGroup group = line.getParametersList().getParamsGroup(GROUP_SCRIPT);
+    assert group != null: "No group " + GROUP_SCRIPT;
+    group.addParameters(myArguments);
+    return line;
+  }
+
+  @NotNull
+  @Override
+  protected List<String> getTestSpecs() {
+    return Collections.emptyList();
+  }
+}
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxConfiguration.java b/python/src/com/jetbrains/python/testing/tox/PyToxConfiguration.java
new file mode 100644 (file)
index 0000000..0d0a079
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2000-2015 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.tox;
+
+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.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import com.intellij.util.xmlb.SkipEmptySerializationFilter;
+import com.intellij.util.xmlb.XmlSerializer;
+import com.intellij.util.xmlb.annotations.Tag;
+import com.jetbrains.serialization.CompoundFilter;
+import com.jetbrains.serialization.AnnotationSerializationFilter;
+import com.jetbrains.python.run.AbstractPythonRunConfiguration;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author Ilya.Kazakevich
+ */
+final class PyToxConfiguration extends AbstractPythonRunConfiguration<PyToxConfiguration> {
+
+  private static final String[] EMPTY_STRINGS = new String[0];
+  @NotNull
+  private final Project myProject;
+
+  @Tag
+  private String[] myArguments;
+
+  PyToxConfiguration(@NotNull final PyToxConfigurationFactory factory, @NotNull final Project project) {
+    super(project, factory);
+    myProject = project;
+    // Module will be stored with XmlSerializer
+    //noinspection AssignmentToSuperclassField
+    mySkipModuleSerialization = true;
+  }
+
+  @NotNull
+  String[] getArguments() {
+    return (myArguments == null ? EMPTY_STRINGS : myArguments.clone());
+  }
+
+  void setArguments(@NotNull final String... arguments) {
+    myArguments = arguments.clone();
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    super.readExternal(element);
+    XmlSerializer.deserializeInto(this, element);
+  }
+
+  @Override
+  public void writeExternal(final Element element) throws WriteExternalException {
+    super.writeExternal(element);
+    XmlSerializer.serializeInto(this, element, new CompoundFilter(
+      new SkipEmptySerializationFilter(),
+      new AnnotationSerializationFilter()
+    ));
+  }
+
+  @Override
+  protected SettingsEditor<PyToxConfiguration> createConfigurationEditor() {
+    return new PyToxConfigurationSettings(myProject);
+  }
+
+  @Nullable
+  @Override
+  public RunProfileState getState(@NotNull final Executor executor, @NotNull final ExecutionEnvironment environment) {
+    return  new PyToxCommandLineState(this, environment, myArguments);
+  }
+}
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationFactory.java b/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationFactory.java
new file mode 100644 (file)
index 0000000..faaae77
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2015 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.tox;
+
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
+import com.jetbrains.python.run.PythonConfigurationFactoryBase;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author Ilya.Kazakevich
+ */
+public final  class PyToxConfigurationFactory extends PythonConfigurationFactoryBase {
+  public static final ConfigurationFactory INSTANCE = new PyToxConfigurationFactory(PyToxConfigurationType.INSTANCE);
+
+  public PyToxConfigurationFactory(@NotNull final ConfigurationType type) {
+    super(type);
+  }
+
+  @Override
+  public String getName() {
+    return "Tox";
+  }
+
+  @NotNull
+  @Override
+  public RunConfiguration createTemplateConfiguration(@NotNull final Project project) {
+    return new PyToxConfiguration(this, project);
+  }
+}
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationProducer.java b/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationProducer.java
new file mode 100644 (file)
index 0000000..a80cbe2
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2015 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.tox;
+
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+
+/**
+ * @author Ilya.Kazakevich
+ */
+public final class PyToxConfigurationProducer extends RunConfigurationProducer<PyToxConfiguration> {
+
+  public PyToxConfigurationProducer() {
+    super(PyToxConfigurationFactory.INSTANCE);
+  }
+
+  @Override
+  public boolean isConfigurationFromContext(PyToxConfiguration configuration, ConfigurationContext context) {
+    return false;
+  }
+
+  @Override
+  protected boolean setupConfigurationFromContext(PyToxConfiguration configuration,
+                                                  ConfigurationContext context,
+                                                  Ref<PsiElement> sourceElement) {
+    return false;
+  }
+}
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationSettings.form b/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationSettings.form
new file mode 100644 (file)
index 0000000..9f5beb9
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.testing.tox.PyToxConfigurationSettings">
+  <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" 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/>
+    <border type="none"/>
+    <children>
+      <component id="e408a" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Tox"/>
+        </properties>
+      </component>
+      <grid id="f677e" 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="1" 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>
+          <vspacer id="a4965">
+            <constraints>
+              <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+            </constraints>
+          </vspacer>
+          <component id="43a1d" class="javax.swing.JTextField" binding="myArgumentsField">
+            <constraints>
+              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+                <preferred-size width="150" height="-1"/>
+              </grid>
+            </constraints>
+            <properties/>
+          </component>
+          <component id="8d4c" class="com.intellij.ui.components.JBLabel">
+            <constraints>
+              <grid row="0" 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="Arguments"/>
+            </properties>
+          </component>
+        </children>
+      </grid>
+    </children>
+  </grid>
+</form>
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationSettings.java b/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationSettings.java
new file mode 100644 (file)
index 0000000..1c97aad
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2015 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.tox;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.jetbrains.python.run.AbstractPyCommonOptionsForm;
+import com.jetbrains.python.run.AbstractPythonRunConfiguration;
+import com.jetbrains.python.run.PyCommonOptionsFormData;
+import com.jetbrains.python.run.PyCommonOptionsFormFactory;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * @author Ilya.Kazakevich
+ */
+final class PyToxConfigurationSettings extends SettingsEditor<PyToxConfiguration> {
+  private static final Pattern ARG_SEPARATOR = Pattern.compile("\\s+");
+  @NotNull
+  private final Project myProject;
+  private AbstractPyCommonOptionsForm myForm;
+  private JPanel myPanel;
+  private JTextField myArgumentsField;
+
+  PyToxConfigurationSettings(@NotNull final Project project) {
+    myProject = project;
+  }
+
+  @Override
+  protected void applyEditorTo(final PyToxConfiguration s) {
+    AbstractPythonRunConfiguration.copyParams(myForm, s);
+    s.setArguments(ARG_SEPARATOR.split(myArgumentsField.getText()));
+  }
+
+  @Override
+  protected void resetEditorFrom(final PyToxConfiguration s) {
+    AbstractPythonRunConfiguration.copyParams(s, myForm);
+    myArgumentsField.setText(StringUtil.join(s.getArguments(), " "));
+  }
+
+  @NotNull
+  @Override
+  protected JComponent createEditor() {
+
+    final JPanel panel = new JPanel(new BorderLayout());
+    panel.add(myPanel, BorderLayout.PAGE_START);
+
+    myForm = createEnvPanel();
+    final JComponent envPanel = myForm.getMainPanel();
+
+    panel.add(envPanel, BorderLayout.PAGE_END);
+
+    return panel;
+  }
+
+  @NotNull
+  private AbstractPyCommonOptionsForm createEnvPanel() {
+    return PyCommonOptionsFormFactory.getInstance().createForm(new PyCommonOptionsFormData() {
+      @Override
+      public Project getProject() {
+        return myProject;
+      }
+
+      @Override
+      public List<Module> getValidModules() {
+        return AbstractPythonRunConfiguration.getValidModules(myProject);
+      }
+
+      @Override
+      public boolean showConfigureInterpretersLink() {
+        return false;
+      }
+    });
+  }
+}
diff --git a/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationType.java b/python/src/com/jetbrains/python/testing/tox/PyToxConfigurationType.java
new file mode 100644 (file)
index 0000000..afe9ef3
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2000-2015 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.tox;
+
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import icons.PythonIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/**
+ * @author Ilya.Kazakevich
+ */
+public class PyToxConfigurationType implements ConfigurationType {
+
+  public static final String ID = "tox";
+  public static final ConfigurationType INSTANCE = new PyToxConfigurationType();
+
+  @Override
+  public ConfigurationFactory[] getConfigurationFactories() {
+    return new ConfigurationFactory[]{new PyToxConfigurationFactory(this)};
+  }
+
+  @Override
+  public String getDisplayName() {
+    return "tox";
+  }
+
+  @Override
+  public String getConfigurationTypeDescription() {
+    return "Tox runner";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return PythonIcons.Python.PythonTests;
+  }
+
+  @NotNull
+  @Override
+  public String getId() {
+    return ID;
+  }
+}
diff --git a/python/src/com/jetbrains/python/testing/tox/package-info.java b/python/src/com/jetbrains/python/testing/tox/package-info.java
new file mode 100644 (file)
index 0000000..f6ca14d
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2000-2015 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.
+ */
+
+/**
+ * <a href="https://tox.readthedocs.org/en/latest/">tox test runner</a>
+ * @author Ilya.Kazakevich
+ */
+package com.jetbrains.python.testing.tox;
\ No newline at end of file
index 8ecc4d60a7d029189ef3d9e693ca516a4fcf53e3..1915371a6f6ac668dd1b70274f22ffbbde72ddfe 100644 (file)
@@ -24,6 +24,7 @@ import com.jetbrains.python.PyNames;
 import com.jetbrains.python.PythonHelper;
 import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration;
 import com.jetbrains.python.testing.PythonTestCommandLineStateBase;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -49,6 +50,7 @@ public class PythonUnitTestCommandLineState extends
     return PythonHelper.UT;
   }
 
+  @NotNull
   protected List<String> getTestSpecs() {
     List<String> specs = new ArrayList<String>();
 
diff --git a/python/src/com/jetbrains/serialization/AnnotationSerializationFilter.java b/python/src/com/jetbrains/serialization/AnnotationSerializationFilter.java
new file mode 100644 (file)
index 0000000..3eaea6c
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2015 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.serialization;
+
+import com.intellij.util.xmlb.Accessor;
+import com.intellij.util.xmlb.SerializationFilterBase;
+import com.intellij.util.xmlb.annotations.Attribute;
+import com.intellij.util.xmlb.annotations.Property;
+import com.intellij.util.xmlb.annotations.Tag;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Only accepts accessors with xmlb tags
+ * @author Ilya.Kazakevich
+ */
+public final class AnnotationSerializationFilter extends SerializationFilterBase {
+  @Override
+  protected boolean accepts(@NotNull final Accessor accessor, @NotNull final Object bean, @Nullable final Object beanValue) {
+    return accessor.getAnnotation(Property.class) != null
+           || accessor.getAnnotation(Tag.class) != null
+           || accessor.getAnnotation(Attribute.class) != null;
+  }
+}
diff --git a/python/src/com/jetbrains/serialization/CompoundFilter.java b/python/src/com/jetbrains/serialization/CompoundFilter.java
new file mode 100644 (file)
index 0000000..58f513f
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2015 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.serialization;
+
+import com.intellij.util.xmlb.Accessor;
+import com.intellij.util.xmlb.SerializationFilter;
+import com.intellij.util.xmlb.SerializationFilterBase;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Returns true only if all filters return true
+ * @author Ilya.Kazakevich
+ */
+public final class CompoundFilter extends SerializationFilterBase {
+  @NotNull
+  private final SerializationFilter[] myFilters;
+
+  public CompoundFilter(@NotNull final SerializationFilter... filters) {
+    myFilters = filters.clone();
+  }
+
+  @Override
+  protected boolean accepts(@NotNull final Accessor accessor, @NotNull final Object bean, @Nullable final Object beanValue) {
+    for (final SerializationFilter filter : myFilters) {
+      if (!filter.accepts(accessor, bean)) {
+        return false;
+      }
+    }
+    return true;
+  }
+}