Skip to content

Commit a891a42

Browse files
Introduce utils class for unittest checks (#1178)
1 parent 587ac7a commit a891a42

File tree

7 files changed

+234
-86
lines changed

7 files changed

+234
-86
lines changed

python-checks/src/main/java/org/sonar/python/checks/tests/ImplicitlySkippedTestCheck.java

Lines changed: 3 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,67 +19,32 @@
1919
*/
2020
package org.sonar.python.checks.tests;
2121

22-
import java.util.ArrayList;
23-
import java.util.Arrays;
2422
import java.util.Collections;
25-
import java.util.HashSet;
2623
import java.util.List;
2724
import java.util.Set;
28-
import java.util.stream.Collectors;
2925
import org.sonar.check.Rule;
3026
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
31-
import org.sonar.plugins.python.api.symbols.ClassSymbol;
32-
import org.sonar.plugins.python.api.symbols.Symbol;
3327
import org.sonar.plugins.python.api.tree.AssertStatement;
3428
import org.sonar.plugins.python.api.tree.AssignmentStatement;
3529
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
3630
import org.sonar.plugins.python.api.tree.CallExpression;
37-
import org.sonar.plugins.python.api.tree.ClassDef;
3831
import org.sonar.plugins.python.api.tree.FunctionDef;
3932
import org.sonar.plugins.python.api.tree.IfStatement;
4033
import org.sonar.plugins.python.api.tree.Name;
4134
import org.sonar.plugins.python.api.tree.QualifiedExpression;
4235
import org.sonar.plugins.python.api.tree.ReturnStatement;
4336
import org.sonar.plugins.python.api.tree.Statement;
4437
import org.sonar.plugins.python.api.tree.Tree;
45-
import org.sonar.python.tree.TreeUtils;
38+
import org.sonar.python.tests.UnittestUtils;
4639

4740
@Rule(key = "S5918")
4841
public class ImplicitlySkippedTestCheck extends PythonSubscriptionCheck {
4942

5043
private static final String MESSAGE = "Skip this test explicitly.";
5144

52-
private static final List<TestFramework> testFrameworks = new ArrayList<>();
53-
54-
static {
55-
// Unit Test method source : https://docs.python.org/2/library/unittest.html#assert-methods
56-
testFrameworks.add(new TestFramework("unittest", Arrays.asList("unittest", "TestCase"), new HashSet<>(Arrays.asList("assertEqual",
57-
"assertNotEqual", "assertTrue", "assertFalse", "assertIs", "assertIsNot", "assertIsNone", "assertIsNotNone", "assertIn",
58-
"assertNotIn", "assertIsInstance", "assertNotIsInstance", "assertRaises", "assertRaisesRegexp", "assertAlmostEqual",
59-
"assertNotAlmostEqual", "assertGreater", "assertGreaterEqual", "assertLess", "assertLessEqual", "assertRegexpMatches",
60-
"assertNotRegexpMatches", "assertItemsEqual", "assertDictContainsSubset", "assertMultiLineEqual", "assertSequenceEqual",
61-
"assertListEqual", "assertTupleEqual", "assertSetEqual", "assertDictEqual"))));
62-
}
63-
6445
private static final Tree.Kind[] literalsKind = {Tree.Kind.STRING_LITERAL, Tree.Kind.NUMERIC_LITERAL, Tree.Kind.LIST_LITERAL,
6546
Tree.Kind.BOOLEAN_LITERAL_PATTERN, Tree.Kind.NUMERIC_LITERAL_PATTERN, Tree.Kind.NONE_LITERAL_PATTERN, Tree.Kind.STRING_LITERAL_PATTERN};
6647

67-
static class TestFramework {
68-
String name;
69-
List<String> keywords;
70-
Set<String> supportedAssertMethods;
71-
72-
public TestFramework(String name, List<String> keywords, Set<String> supportedAssertMethods) {
73-
this.name = name;
74-
this.keywords = keywords;
75-
this.supportedAssertMethods = supportedAssertMethods;
76-
}
77-
78-
public boolean matchAnyProvidedClasses(List<String> classes) {
79-
return classes.stream().anyMatch(parentClass -> keywords.stream().allMatch(parentClass::contains));
80-
}
81-
}
82-
8348
@Override
8449
public void initialize(Context context) {
8550
context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, ctx -> {
@@ -128,45 +93,13 @@ private static ReturnStatement getReturnStatementFromFirstIfStatement(FunctionDe
12893
}
12994

13095
private static boolean containsAssertion(FunctionDef functionDef) {
131-
Set<String> supportedAssertMethods = getParentClassTestFrameworkFromFunctionDef(functionDef);
96+
Set<String> supportedAssertMethods = UnittestUtils.isWithinUnittestTestCase(functionDef) ?
97+
UnittestUtils.ASSERTIONS_METHODS : Collections.emptySet();
13298
AssertionVisitor assertVisitor = new AssertionVisitor(supportedAssertMethods);
13399
functionDef.accept(assertVisitor);
134100
return assertVisitor.hasAnAssert;
135101
}
136102

137-
private static Set<String> getParentClassTestFrameworkFromFunctionDef(FunctionDef functionDef) {
138-
List<String> libraries = new ArrayList<>();
139-
ClassDef classDef = (ClassDef) TreeUtils.firstAncestorOfKind(functionDef, Tree.Kind.CLASSDEF);
140-
if (classDef != null) {
141-
libraries.addAll(getInheritedClassesFQN(classDef));
142-
}
143-
144-
return testFrameworks.stream()
145-
.filter(testFramework -> testFramework.matchAnyProvidedClasses(libraries))
146-
.findFirst()
147-
.map(t -> t.supportedAssertMethods)
148-
.orElseGet(Collections::emptySet);
149-
}
150-
151-
private static List<String> getInheritedClassesFQN(ClassDef classDefinition) {
152-
return getParentClasses(TreeUtils.getClassSymbolFromDef(classDefinition)).stream()
153-
.map(Symbol::fullyQualifiedName)
154-
.collect(Collectors.toList());
155-
}
156-
157-
private static List<Symbol> getParentClasses(ClassSymbol classSymbol) {
158-
List<Symbol> superclasses = new ArrayList<>();
159-
if (classSymbol != null) {
160-
for (Symbol symbol : classSymbol.superClasses()) {
161-
superclasses.add(symbol);
162-
if (symbol instanceof ClassSymbol) {
163-
superclasses.addAll(getParentClasses((ClassSymbol) symbol));
164-
}
165-
}
166-
}
167-
return superclasses;
168-
}
169-
170103
static class AssertionVisitor extends BaseTreeVisitor {
171104
boolean hasAnAssert = false;
172105
Set<String> supportedMethods;

python-checks/src/main/java/org/sonar/python/checks/tests/NotDiscoverableTestMethodCheck.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import java.util.Map;
2626
import java.util.Optional;
2727
import java.util.Set;
28-
import java.util.stream.Collectors;
2928
import org.sonar.check.Rule;
3029
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
3130
import org.sonar.plugins.python.api.SubscriptionContext;
@@ -37,26 +36,13 @@
3736
import org.sonar.plugins.python.api.tree.FunctionDef;
3837
import org.sonar.plugins.python.api.tree.Statement;
3938
import org.sonar.plugins.python.api.tree.Tree;
39+
import org.sonar.python.tests.UnittestUtils;
4040
import org.sonar.python.tree.TreeUtils;
4141

42-
import static java.util.stream.Stream.concat;
43-
4442
@Rule(key = "S5899")
4543
public class NotDiscoverableTestMethodCheck extends PythonSubscriptionCheck {
4644

4745
private static final String MESSAGE = "Rename this method so that it starts with \"test\" or remove this unused helper.";
48-
// All methods of unittest https://docs.python.org/3/library/unittest.html#unittest.TestCase
49-
private static final List<String> UNITTEST_RUN_METHODS = List.of("setUp", "tearDown", "setUpClass", "tearDownClass", "run", "skiptTest",
50-
"subTest", "debug");
51-
private static final List<String> UNITTEST_CHECK_METHODS = List.of("assertEqual", "assertNotEqual", "assertTrue", "assertFalse",
52-
"assertIs", "assertIsNot", "assertIsNone", "assertIsNotNone", "assertIn", "assertNotIn", "assertIsInstance", "assertNotIsInstance",
53-
"assertRaises", "assertRaisesRegex", "assertWarns", "assertWarnsRegex", "assertLogs", "assertNoLogs", "assertAlmostEqual", "assertGreater",
54-
"assertGreaterEqual", "assertLess", "assertLessEqual", "assertRegex", "assertNotRegex", "assertCountEqual", "addTypeEqualityFunc",
55-
"fail", "failureException", "longMessage", "maxDiff");
56-
private static final List<String> UNITTEST_GATHER_INFO = List.of("countTestCases", "defaultTestResult", "id", "shortDescription", "addCleanup",
57-
"doCleanups", "addClassCleanup", "doClassCleanups");
58-
private static final List<String> UNITTEST_METHODS = concat(concat(UNITTEST_CHECK_METHODS.stream(), UNITTEST_RUN_METHODS.stream()), UNITTEST_GATHER_INFO.stream())
59-
.collect(Collectors.toList());
6046

6147
@Override
6248
public void initialize(Context context) {
@@ -114,7 +100,7 @@ private static boolean inheritsOnlyFromUnitTest(ClassDef classDefinition) {
114100
}
115101

116102
private static boolean overrideExistingMethod(String functionName) {
117-
return UNITTEST_METHODS.contains(functionName) || functionName.startsWith("_");
103+
return UnittestUtils.allMethods().contains(functionName) || functionName.startsWith("_");
118104
}
119105

120106
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.tests;
21+
22+
import java.util.ArrayList;
23+
import java.util.Collections;
24+
import java.util.HashSet;
25+
import java.util.List;
26+
import java.util.Set;
27+
import org.sonar.plugins.python.api.tree.ClassDef;
28+
import org.sonar.plugins.python.api.tree.Tree;
29+
import org.sonar.python.tree.TreeUtils;
30+
31+
public class UnittestUtils {
32+
33+
private UnittestUtils() {
34+
35+
}
36+
37+
// Methods of unittest are the union of Python 2 and Python 3 methods:
38+
// https://docs.python.org/2/library/unittest.html#unittest.TestCase
39+
// https://docs.python.org/3/library/unittest.html#unittest.TestCase
40+
public static final Set<String> RUN_METHODS = Set.of("setUp", "tearDown", "setUpClass", "tearDownClass", "run", "skiptTest",
41+
"subTest", "debug");
42+
43+
public static final Set<String> ASSERTIONS_METHODS = Set.of("assertEqual",
44+
"assertNotEqual", "assertTrue", "assertFalse", "assertIs", "assertIsNot", "assertIsNone", "assertIsNotNone", "assertIn",
45+
"assertNotIn", "assertIsInstance", "assertNotIsInstance", "assertRaises", "assertRaisesRegexp", "assertAlmostEqual",
46+
"assertNotAlmostEqual", "assertGreater", "assertGreaterEqual", "assertLess", "assertLessEqual", "assertRegexpMatches",
47+
"assertNotRegexpMatches", "assertItemsEqual", "assertDictContainsSubset", "assertMultiLineEqual", "assertSequenceEqual",
48+
"assertListEqual", "assertTupleEqual", "assertSetEqual", "assertDictEqual", "assertRaisesRegex", "assertWarns", "assertWarnsRegex",
49+
"assertLogs", "assertNoLogs", "assertRegex", "assertNotRegex", "assertCountEqual");
50+
51+
public static final Set<String> UTIL_METHODS = Set.of("addTypeEqualityFunc", "fail", "failureException", "longMessage", "maxDiff");
52+
53+
public static final Set<String> GATHER_INFO_METHODS = Set.of("countTestCases", "defaultTestResult", "id", "shortDescription", "addCleanup",
54+
"doCleanups", "addClassCleanup", "doClassCleanups");
55+
56+
private static final Set<String> ALL_METHODS = new HashSet<>();
57+
58+
static {
59+
ALL_METHODS.addAll(RUN_METHODS);
60+
ALL_METHODS.addAll(UTIL_METHODS);
61+
ALL_METHODS.addAll(GATHER_INFO_METHODS);
62+
ALL_METHODS.addAll(ASSERTIONS_METHODS);
63+
}
64+
65+
public static Set<String> allMethods() {
66+
return Collections.unmodifiableSet(ALL_METHODS);
67+
}
68+
69+
public static boolean isWithinUnittestTestCase(Tree tree) {
70+
List<String> parentClassesFQN = new ArrayList<>();
71+
ClassDef classDef = (ClassDef) TreeUtils.firstAncestorOfKind(tree, Tree.Kind.CLASSDEF);
72+
if (classDef != null) {
73+
parentClassesFQN.addAll(TreeUtils.getParentClassesFQN(classDef));
74+
}
75+
return parentClassesFQN.stream().anyMatch(parentClass -> parentClass.contains("unittest") && parentClass.contains("TestCase"));
76+
}
77+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
@ParametersAreNonnullByDefault
21+
package org.sonar.python.tests;
22+
23+
import javax.annotation.ParametersAreNonnullByDefault;
24+

python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Collections;
2424
import java.util.EnumSet;
2525
import java.util.List;
26+
import java.util.Objects;
2627
import java.util.Optional;
2728
import java.util.Set;
2829
import java.util.function.Predicate;
@@ -142,6 +143,27 @@ public static ClassSymbol getClassSymbolFromDef(@Nullable ClassDef classDef) {
142143
return null;
143144
}
144145

146+
public static List<String> getParentClassesFQN(ClassDef classDef) {
147+
return getParentClasses(TreeUtils.getClassSymbolFromDef(classDef)).stream()
148+
.map(Symbol::fullyQualifiedName)
149+
.filter(Objects::nonNull)
150+
.collect(Collectors.toList());
151+
}
152+
153+
private static List<Symbol> getParentClasses(@Nullable ClassSymbol classSymbol) {
154+
List<Symbol> superClasses = new ArrayList<>();
155+
if (classSymbol == null) {
156+
return superClasses;
157+
}
158+
for (Symbol symbol : classSymbol.superClasses()) {
159+
superClasses.add(symbol);
160+
if (symbol instanceof ClassSymbol) {
161+
superClasses.addAll(getParentClasses((ClassSymbol) symbol));
162+
}
163+
}
164+
return superClasses;
165+
}
166+
145167
@CheckForNull
146168
public static FunctionSymbol getFunctionSymbolFromDef(@Nullable FunctionDef functionDef) {
147169
if (functionDef == null) {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.tests;
21+
22+
23+
import org.junit.Test;
24+
import org.sonar.plugins.python.api.tree.FileInput;
25+
import org.sonar.plugins.python.api.tree.Tree;
26+
import org.sonar.python.PythonTestUtils;
27+
import org.sonar.python.semantic.SymbolTableBuilder;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.sonar.python.PythonTestUtils.pythonFile;
31+
32+
public class UnittestUtilsTest {
33+
34+
@Test
35+
public void test_isWithinUnittestTestCase() {
36+
String code = "import unittest\nclass A(unittest.TestCase): ...";
37+
FileInput fileInput = PythonTestUtils.parse(new SymbolTableBuilder("", pythonFile("mod1.py")), code);
38+
Tree tree = PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.ELLIPSIS));
39+
assertThat(UnittestUtils.isWithinUnittestTestCase(tree)).isTrue();
40+
41+
code = "import unittest\nclass A(unittest.case.TestCase): ...";
42+
fileInput = PythonTestUtils.parse(new SymbolTableBuilder("", pythonFile("mod1.py")), code);
43+
tree = PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.ELLIPSIS));
44+
assertThat(UnittestUtils.isWithinUnittestTestCase(tree)).isTrue();
45+
46+
code = "import random_wrapper\nclass A(random_wrapper.unittest.TestCase): ...";
47+
fileInput = PythonTestUtils.parse(new SymbolTableBuilder("", pythonFile("mod1.py")), code);
48+
tree = PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.ELLIPSIS));
49+
assertThat(UnittestUtils.isWithinUnittestTestCase(tree)).isTrue();
50+
51+
code = "import random\nclass A(random.TestCase): ...";
52+
fileInput = PythonTestUtils.parse(new SymbolTableBuilder("", pythonFile("mod1.py")), code);
53+
tree = PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.ELLIPSIS));
54+
assertThat(UnittestUtils.isWithinUnittestTestCase(tree)).isFalse();
55+
56+
code = "import unittest\nclass A(unittest.other): ...";
57+
fileInput = PythonTestUtils.parse(new SymbolTableBuilder("", pythonFile("mod1.py")), code);
58+
tree = PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.ELLIPSIS));
59+
assertThat(UnittestUtils.isWithinUnittestTestCase(tree)).isFalse();
60+
61+
code = "...";
62+
fileInput = PythonTestUtils.parse(new SymbolTableBuilder("", pythonFile("mod1.py")), code);
63+
tree = PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.ELLIPSIS));
64+
assertThat(UnittestUtils.isWithinUnittestTestCase(tree)).isFalse();
65+
}
66+
67+
@Test
68+
public void all_methods() {
69+
assertThat(UnittestUtils.allMethods()).hasSize(59);
70+
}
71+
}

0 commit comments

Comments
 (0)