mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-88533 Capture logging output — show only for failing tests
Enable skippassedoutput in teamcity pytest plugin. Pytest's built-in log capture collects logging output per test. With skippassedoutput, log output only appears for failing tests. (cherry picked from commit dab8b63d5221e76ab8a53d37396df9c3ee777464) IJ-MR-206446 GitOrigin-RevId: b33b2e1b75910cd128fdfff6f22f617317b947fa
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8ea37bb823
commit
7ab4e19b14
@@ -25,6 +25,8 @@ from teamcity import pytest_plugin
|
||||
import os
|
||||
|
||||
_DOCTEST_MODULES_ARG = "--doctest-modules"
|
||||
_JB_SKIP_PASSED_OUTPUT_DEFAULT_ARG = "--jb-skippassedoutput-default"
|
||||
_JB_REPORT_LOGS_AS_TEST_LOG_ARG = "--jb-report-logs-as-test-log"
|
||||
|
||||
def _add_module_to_target(module_name, python_parts):
|
||||
# Doctest: Find the fully qualified name of the target module by checking each
|
||||
@@ -42,6 +44,13 @@ def _add_module_to_target(module_name, python_parts):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
skip_passed_output_default = _JB_SKIP_PASSED_OUTPUT_DEFAULT_ARG in sys.argv
|
||||
while _JB_SKIP_PASSED_OUTPUT_DEFAULT_ARG in sys.argv:
|
||||
sys.argv.remove(_JB_SKIP_PASSED_OUTPUT_DEFAULT_ARG)
|
||||
report_logs_as_test_log = _JB_REPORT_LOGS_AS_TEST_LOG_ARG in sys.argv
|
||||
while _JB_REPORT_LOGS_AS_TEST_LOG_ARG in sys.argv:
|
||||
sys.argv.remove(_JB_REPORT_LOGS_AS_TEST_LOG_ARG)
|
||||
|
||||
path, targets, additional_args = parse_arguments()
|
||||
sys.argv += additional_args
|
||||
|
||||
@@ -65,6 +74,11 @@ if __name__ == '__main__':
|
||||
plugins_to_load.append(pytest_plugin)
|
||||
|
||||
args = sys.argv[1:]
|
||||
if skip_passed_output_default:
|
||||
pytest_plugin.set_skip_passed_output_default(True)
|
||||
if report_logs_as_test_log:
|
||||
pytest_plugin.set_report_logs_as_test_log(True)
|
||||
|
||||
if "--jb-show-summary" in args:
|
||||
args.remove("--jb-show-summary")
|
||||
elif int(pytest.__version__.split('.')[0]) >= 6:
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.intellij.util.execution.ParametersListUtil
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonHelper
|
||||
import com.jetbrains.python.psi.resolve.PackageAvailabilitySpec
|
||||
import com.jetbrains.python.run.PythonScriptExecution
|
||||
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
|
||||
import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant
|
||||
import com.jetbrains.python.testing.PyTestSharedForm.create
|
||||
@@ -50,11 +51,22 @@ class PyPyTestExecutionEnvironment(configuration: PyTestConfiguration, environme
|
||||
}
|
||||
|
||||
override fun customizePythonExecutionEnvironmentVars(helpersAwareTargetRequest: HelpersAwareTargetEnvironmentRequest,
|
||||
envs: MutableMap<String, TargetEnvironmentFunction<String>>,
|
||||
passParentEnvs: Boolean) {
|
||||
envs: MutableMap<String, TargetEnvironmentFunction<String>>,
|
||||
passParentEnvs: Boolean) {
|
||||
super.customizePythonExecutionEnvironmentVars(helpersAwareTargetRequest, envs, passParentEnvs)
|
||||
envs[PYTEST_RUN_CONFIG] = constant("True")
|
||||
}
|
||||
|
||||
override fun addAfterParameters(cmd: GeneralCommandLine) {
|
||||
cmd.parametersList.getParamsGroup(GROUP_SCRIPT)?.addParameter(PYTEST_SKIP_PASSED_OUTPUT_DEFAULT_ARG)
|
||||
cmd.parametersList.getParamsGroup(GROUP_SCRIPT)?.addParameter(PYTEST_REPORT_LOGS_AS_TEST_LOG_ARG)
|
||||
}
|
||||
|
||||
override fun addAfterParameters(targetEnvironmentRequest: TargetEnvironmentRequest,
|
||||
testScriptExecution: PythonScriptExecution) {
|
||||
testScriptExecution.addParameter(constant(PYTEST_SKIP_PASSED_OUTPUT_DEFAULT_ARG))
|
||||
testScriptExecution.addParameter(constant(PYTEST_REPORT_LOGS_AS_TEST_LOG_ARG))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,3 +162,5 @@ class PyTestFactory(type: PythonTestConfigurationType) : PyAbstractTestFactory<P
|
||||
}
|
||||
|
||||
private const val PYTEST_RUN_CONFIG: String = "PYTEST_RUN_CONFIG"
|
||||
private const val PYTEST_SKIP_PASSED_OUTPUT_DEFAULT_ARG: String = "--jb-skippassedoutput-default"
|
||||
private const val PYTEST_REPORT_LOGS_AS_TEST_LOG_ARG: String = "--jb-report-logs-as-test-log"
|
||||
|
||||
@@ -3,22 +3,34 @@ package com.jetbrains.python.testing;
|
||||
|
||||
import com.intellij.execution.Executor;
|
||||
import com.intellij.execution.configurations.ModuleRunConfiguration;
|
||||
import com.intellij.execution.testframework.TestConsoleProperties;
|
||||
import com.intellij.execution.testframework.sm.SMCustomMessagesParsing;
|
||||
import com.intellij.execution.testframework.sm.runner.GeneralIdBasedToSMTRunnerEventsConvertor;
|
||||
import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter;
|
||||
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties;
|
||||
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsAdapter;
|
||||
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener;
|
||||
import com.intellij.execution.testframework.sm.runner.SMTestLocator;
|
||||
import com.intellij.execution.testframework.sm.runner.SMTestProxy;
|
||||
import com.intellij.execution.testframework.sm.runner.events.TestDurationStrategy;
|
||||
import com.intellij.execution.testframework.sm.runner.events.TestOutputEvent;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import jetbrains.buildServer.messages.serviceMessages.ServiceMessage;
|
||||
import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
/**
|
||||
* @author Roman.Chernyatchik
|
||||
*/
|
||||
public class PythonTRunnerConsoleProperties extends SMTRunnerConsoleProperties {
|
||||
public class PythonTRunnerConsoleProperties extends SMTRunnerConsoleProperties implements SMCustomMessagesParsing {
|
||||
public static final String FRAMEWORK_NAME = "PythonUnitTestRunner";
|
||||
private static final String TEST_LOG_SERVICE_MESSAGE = "testLog";
|
||||
private static final String NAME_ATTRIBUTE = "name";
|
||||
private static final String NODE_ID_ATTRIBUTE = "nodeId";
|
||||
private static final String OUT_ATTRIBUTE = "out";
|
||||
|
||||
private final boolean myIsEditable;
|
||||
private final SMTestLocator myLocator;
|
||||
@@ -45,6 +57,12 @@ public class PythonTRunnerConsoleProperties extends SMTRunnerConsoleProperties {
|
||||
return myLocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputToGeneralTestEventsConverter createTestEventsConverter(@NotNull String testFrameworkName,
|
||||
@NotNull TestConsoleProperties consoleProperties) {
|
||||
return new PythonOutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes configuration id-based,
|
||||
*
|
||||
@@ -79,4 +97,27 @@ public class PythonTRunnerConsoleProperties extends SMTRunnerConsoleProperties {
|
||||
return PyBundle.message("runcfg.tests.empty_suite");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PythonOutputToGeneralTestEventsConverter extends OutputToGeneralTestEventsConverter {
|
||||
private PythonOutputToGeneralTestEventsConverter(@NotNull String testFrameworkName,
|
||||
@NotNull TestConsoleProperties consoleProperties) {
|
||||
super(testFrameworkName, consoleProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processServiceMessage(@NotNull ServiceMessage message,
|
||||
@NotNull ServiceMessageVisitor visitor) throws ParseException {
|
||||
if (TEST_LOG_SERVICE_MESSAGE.equals(message.getMessageName())) {
|
||||
String testName = message.getAttributes().get(NAME_ATTRIBUTE);
|
||||
String nodeId = message.getAttributes().get(NODE_ID_ATTRIBUTE);
|
||||
String output = message.getAttributes().get(OUT_ATTRIBUTE);
|
||||
var processor = getProcessor();
|
||||
if (testName != null && output != null && processor != null) {
|
||||
processor.onTestOutput(new TestOutputEvent(testName, nodeId, output, true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
super.processServiceMessage(message, visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
skippassedoutput = false
|
||||
@@ -0,0 +1,7 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_fail_with_logging():
|
||||
logger.warning("warning_from_failing_test")
|
||||
assert False, "intentional failure"
|
||||
@@ -0,0 +1,7 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_pass_with_logging():
|
||||
logger.warning("warning_from_passing_test")
|
||||
assert True
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_pass_with_logging(pytestconfig):
|
||||
assert "PYTEST_TEAMCITY_SKIP_PASSED_OUTPUT_DEFAULT" not in os.environ
|
||||
assert pytestconfig.getini("skippassedoutput") is True
|
||||
logger.warning("warning_from_passing_test")
|
||||
assert True
|
||||
@@ -1413,6 +1413,97 @@ public final class PythonPyTestingTest extends PyEnvTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingCapturedForFailingTest() {
|
||||
runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/capture_logging", SdkCreationType.EMPTY_SDK) {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyTestTestProcessRunner createProcessRunner() {
|
||||
return new PyTestTestProcessRunner("test_logging_fail.py", 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
|
||||
@NotNull final String stdout,
|
||||
@NotNull final String stderr,
|
||||
@NotNull final String all,
|
||||
int exitCode) {
|
||||
var consoleText = runner.getAllConsoleText();
|
||||
Assertions.assertThat(consoleText.split("\n|(\r\n)"))
|
||||
.containsSubsequence(
|
||||
"test_logging_fail.py::test_fail_with_logging FAILED [100%]",
|
||||
"WARNING test_logging_fail:test_logging_fail.py:6 warning_from_failing_test"
|
||||
);
|
||||
MatcherAssert.assertThat("Captured logs should use a dedicated service message",
|
||||
all, containsString("[testLog"));
|
||||
MatcherAssert.assertThat("Captured logs should not be reported as stdout",
|
||||
all, not(containsString("[testStdOut")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingSkippedForPassingTest() {
|
||||
runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/capture_logging_default", SdkCreationType.EMPTY_SDK) {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyTestTestProcessRunner createProcessRunner() {
|
||||
return new PyTestTestProcessRunner("test_logging_pass.py", 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
|
||||
@NotNull final String stdout,
|
||||
@NotNull final String stderr,
|
||||
@NotNull final String all,
|
||||
int exitCode) {
|
||||
var consoleText = runner.getAllConsoleText();
|
||||
Assertions.assertThat(consoleText)
|
||||
.doesNotContain("warning_from_passing_test");
|
||||
MatcherAssert.assertThat("Captured logs from passing tests should not leak into the process output",
|
||||
all, not(containsString("warning_from_passing_test")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingCapturedForPassingTestWhenConfigured() {
|
||||
runPythonTest(new PyProcessWithConsoleTestTask<PyTestTestProcessRunner>("/testRunner/env/pytest/capture_logging", SdkCreationType.EMPTY_SDK) {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected PyTestTestProcessRunner createProcessRunner() {
|
||||
return new PyTestTestProcessRunner("test_logging_pass.py", 0) {
|
||||
@Override
|
||||
protected void configurationCreatedAndWillLaunch(@NotNull PyTestConfiguration configuration) throws IOException {
|
||||
super.configurationCreatedAndWillLaunch(configuration);
|
||||
configuration.setAdditionalArguments("-c pytest.ini");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner,
|
||||
@NotNull final String stdout,
|
||||
@NotNull final String stderr,
|
||||
@NotNull final String all,
|
||||
int exitCode) {
|
||||
var consoleText = runner.getAllConsoleText();
|
||||
Assertions.assertThat(consoleText.split("\n|(\r\n)"))
|
||||
.containsSubsequence(
|
||||
"test_logging_pass.py::test_pass_with_logging PASSED [100%]",
|
||||
"WARNING test_logging_pass:test_logging_pass.py:6 warning_from_passing_test"
|
||||
);
|
||||
MatcherAssert.assertThat("Captured logs should use a dedicated service message",
|
||||
all, containsString("[testLog"));
|
||||
MatcherAssert.assertThat("Captured logs should not be reported as stdout",
|
||||
all, not(containsString("[testStdOut")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getFrameworkId() {
|
||||
return PyTestFactory.id;
|
||||
|
||||
Reference in New Issue
Block a user