Skip to content

Commit e15fa21

Browse files
marc-jasper-sonarsourcesonartech
authored andcommitted
SONARPY-3783 Add number of miss-classified test lines telemetry (#851)
GitOrigin-RevId: c3626bfa01d28e93bac45e5f4c2de19741c6c846
1 parent 939aff6 commit e15fa21

6 files changed

Lines changed: 156 additions & 17 deletions

File tree

python-commons/src/main/java/org/sonar/plugins/python/PythonSensor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ private void updateTestFileTelemetry(PythonScanner scanner) {
202202
TestFileTelemetry telemetry = scanner.getTestFileTelemetry();
203203
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_MAIN_FILES_TOTAL, telemetry.totalMainFiles());
204204
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_MAIN_FILES_MISCLASSIFIED_TEST, telemetry.misclassifiedTestFiles());
205+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_LINES_TOTAL, telemetry.totalLines());
206+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_MAIN_LINES, telemetry.totalMainLines());
207+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TEST_LINES, telemetry.testLines());
208+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_MAIN_LINES_MISCLASSIFIED_TEST, telemetry.misclassifiedTestLines());
205209
}
206210

207211
private void updateNamespacePackageTelemetry(PythonIndexer pythonIndexer) {

python-commons/src/main/java/org/sonar/plugins/python/telemetry/TelemetryMetricKey.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ public enum TelemetryMetricKey {
4949
PYTHON_TYPES_SYMBOLS_UNIQUE("python.types.symbols.unique"),
5050
PYTHON_TYPES_SYMBOLS_UNKNOWN("python.types.symbols.unknown"),
5151
PYTHON_MAIN_FILES_TOTAL("python.files.main.total"),
52-
PYTHON_MAIN_FILES_MISCLASSIFIED_TEST("python.files.main.misclassified_test");
52+
PYTHON_MAIN_FILES_MISCLASSIFIED_TEST("python.files.main.misclassified_test"),
53+
PYTHON_LINES_TOTAL("python.lines.total"),
54+
PYTHON_MAIN_LINES("python.lines.main"),
55+
PYTHON_TEST_LINES("python.lines.test"),
56+
PYTHON_MAIN_LINES_MISCLASSIFIED_TEST("python.lines.main.misclassified_test");
5357

5458
private final String key;
5559

python-commons/src/main/java/org/sonar/plugins/python/telemetry/collectors/TestFileTelemetry.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,32 @@
2020
* Telemetry data for tracking test file misclassification.
2121
*
2222
* @param totalMainFiles Total number of files classified as MAIN
23-
* @param misclassifiedTestFiles Number of MAIN files that import unittest or pytest (likely test files)
23+
* @param misclassifiedTestFiles Number of MAIN files that appear to be test files (import unittest/pytest or follow pytest patterns)
24+
* @param totalLines Total number of lines across all files (MAIN + TEST)
25+
* @param totalMainLines Total number of lines across all MAIN files
26+
* @param testLines Number of lines across all TEST files (as classified by the scanner engine)
27+
* @param misclassifiedTestLines Number of lines across misclassified test files (subset of totalMainLines)
2428
*/
2529
public record TestFileTelemetry(
2630
long totalMainFiles,
27-
long misclassifiedTestFiles) {
31+
long misclassifiedTestFiles,
32+
long totalLines,
33+
long totalMainLines,
34+
long testLines,
35+
long misclassifiedTestLines) {
2836

2937
public static TestFileTelemetry empty() {
30-
return new TestFileTelemetry(0, 0);
38+
return new TestFileTelemetry(0, 0, 0, 0, 0, 0);
3139
}
3240

3341
public TestFileTelemetry add(TestFileTelemetry other) {
3442
return new TestFileTelemetry(
3543
this.totalMainFiles + other.totalMainFiles,
36-
this.misclassifiedTestFiles + other.misclassifiedTestFiles
44+
this.misclassifiedTestFiles + other.misclassifiedTestFiles,
45+
this.totalLines + other.totalLines,
46+
this.totalMainLines + other.totalMainLines,
47+
this.testLines + other.testLines,
48+
this.misclassifiedTestLines + other.misclassifiedTestLines
3749
);
3850
}
3951
}

python-commons/src/main/java/org/sonar/plugins/python/telemetry/collectors/TestFileTelemetryCollector.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,53 @@ public class TestFileTelemetryCollector {
3232

3333
private final AtomicLong totalMainFiles = new AtomicLong(0);
3434
private final AtomicLong misclassifiedTestFiles = new AtomicLong(0);
35+
private final AtomicLong totalLines = new AtomicLong(0);
36+
private final AtomicLong totalMainLines = new AtomicLong(0);
37+
private final AtomicLong testLines = new AtomicLong(0);
38+
private final AtomicLong misclassifiedTestLines = new AtomicLong(0);
3539

3640
public void collect(FileInput rootTree, InputFile.Type fileType) {
37-
if (fileType != InputFile.Type.MAIN) {
41+
long lines = lineCount(rootTree);
42+
totalLines.addAndGet(lines);
43+
44+
if (fileType == InputFile.Type.TEST) {
45+
testLines.addAndGet(lines);
3846
return;
3947
}
4048

4149
totalMainFiles.incrementAndGet();
50+
totalMainLines.addAndGet(lines);
51+
52+
if (isMisclassifiedTestFile(rootTree)) {
53+
misclassifiedTestFiles.incrementAndGet();
54+
misclassifiedTestLines.addAndGet(lines);
55+
}
56+
}
4257

58+
private static boolean isMisclassifiedTestFile(FileInput rootTree) {
4359
var importVisitor = new TestImportVisitor();
4460
rootTree.accept(importVisitor);
45-
4661
if (importVisitor.hasTestFrameworkImport) {
47-
misclassifiedTestFiles.incrementAndGet();
48-
return;
62+
return true;
4963
}
5064

5165
var pytestPatternVisitor = new PytestPatternVisitor();
5266
rootTree.accept(pytestPatternVisitor);
67+
return pytestPatternVisitor.hasPytestPattern;
68+
}
5369

54-
if (pytestPatternVisitor.hasPytestPattern) {
55-
misclassifiedTestFiles.incrementAndGet();
56-
}
70+
static long lineCount(FileInput rootTree) {
71+
return rootTree.lastToken().line();
5772
}
5873

5974
public TestFileTelemetry getTelemetry() {
60-
return new TestFileTelemetry(totalMainFiles.get(), misclassifiedTestFiles.get());
75+
return new TestFileTelemetry(
76+
totalMainFiles.get(),
77+
misclassifiedTestFiles.get(),
78+
totalLines.get(),
79+
totalMainLines.get(),
80+
testLines.get(),
81+
misclassifiedTestLines.get());
6182
}
6283

6384
private static class TestImportVisitor extends BaseTreeVisitor {

python-commons/src/test/java/org/sonar/plugins/python/telemetry/collectors/TestFileTelemetryCollectorTest.java

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ private static PythonVisitorContext createContext(String code) {
3636
return TestPythonVisitorRunner.createContext(mockFile, null, "", ProjectLevelSymbolTable.empty(), CacheContextImpl.dummyCache());
3737
}
3838

39+
@Test
40+
void lineCountForSingleLineFile() {
41+
var singleLine = createContext("x = 1");
42+
assertThat(TestFileTelemetryCollector.lineCount(singleLine.rootTree())).isEqualTo(1);
43+
}
44+
45+
@Test
46+
void lineCountForMultiLineFile() {
47+
var multiLine = createContext("""
48+
x = 1
49+
y = 2
50+
z = 3
51+
""");
52+
assertThat(TestFileTelemetryCollector.lineCount(multiLine.rootTree())).isEqualTo(4);
53+
}
54+
3955
@ParameterizedTest
4056
@MethodSource("provideNotMisclassifiedTestCases")
4157
void mainFile_notMisclassified(String code) {
@@ -47,6 +63,10 @@ void mainFile_notMisclassified(String code) {
4763
var telemetry = collector.getTelemetry();
4864
assertThat(telemetry.totalMainFiles()).isEqualTo(1);
4965
assertThat(telemetry.misclassifiedTestFiles()).isZero();
66+
assertThat(telemetry.totalLines()).isPositive();
67+
assertThat(telemetry.totalMainLines()).isEqualTo(telemetry.totalLines());
68+
assertThat(telemetry.testLines()).isZero();
69+
assertThat(telemetry.misclassifiedTestLines()).isZero();
5070
}
5171

5272
private static Stream<Arguments> provideNotMisclassifiedTestCases() {
@@ -77,6 +97,10 @@ void mainFileWithTestImports_isMisclassified(String code) {
7797
var telemetry = collector.getTelemetry();
7898
assertThat(telemetry.totalMainFiles()).isEqualTo(1);
7999
assertThat(telemetry.misclassifiedTestFiles()).isEqualTo(1);
100+
assertThat(telemetry.totalLines()).isPositive();
101+
assertThat(telemetry.totalMainLines()).isEqualTo(telemetry.totalLines());
102+
assertThat(telemetry.testLines()).isZero();
103+
assertThat(telemetry.misclassifiedTestLines()).isEqualTo(telemetry.totalMainLines());
80104
}
81105

82106
private static Stream<Arguments> provideMisclassifiedTestCases() {
@@ -123,7 +147,7 @@ def my_fixture():
123147

124148
@ParameterizedTest
125149
@MethodSource("provideTestFileNotCountedCases")
126-
void testFile_notCounted(String code) {
150+
void testFile_countsLinesButNotMainMetrics(String code) {
127151
var context = createContext(code);
128152

129153
var collector = new TestFileTelemetryCollector();
@@ -132,6 +156,10 @@ void testFile_notCounted(String code) {
132156
var telemetry = collector.getTelemetry();
133157
assertThat(telemetry.totalMainFiles()).isZero();
134158
assertThat(telemetry.misclassifiedTestFiles()).isZero();
159+
assertThat(telemetry.totalLines()).isPositive();
160+
assertThat(telemetry.totalMainLines()).isZero();
161+
assertThat(telemetry.testLines()).isEqualTo(telemetry.totalLines());
162+
assertThat(telemetry.misclassifiedTestLines()).isZero();
135163
}
136164

137165
private static Stream<Arguments> provideTestFileNotCountedCases() {
@@ -189,6 +217,10 @@ void aggregatesAcrossMultipleFiles() {
189217
var telemetry = collector.getTelemetry();
190218
assertThat(telemetry.totalMainFiles()).isEqualTo(3);
191219
assertThat(telemetry.misclassifiedTestFiles()).isEqualTo(2);
220+
assertThat(telemetry.totalLines()).isEqualTo(6);
221+
assertThat(telemetry.totalMainLines()).isEqualTo(6);
222+
assertThat(telemetry.testLines()).isZero();
223+
assertThat(telemetry.misclassifiedTestLines()).isEqualTo(4);
192224
}
193225

194226
@Test
@@ -205,6 +237,10 @@ void mixedMainAndTestFiles() {
205237
var telemetry = collector.getTelemetry();
206238
assertThat(telemetry.totalMainFiles()).isEqualTo(2);
207239
assertThat(telemetry.misclassifiedTestFiles()).isEqualTo(1);
240+
assertThat(telemetry.totalLines()).isEqualTo(3);
241+
assertThat(telemetry.totalMainLines()).isEqualTo(2);
242+
assertThat(telemetry.testLines()).isEqualTo(1);
243+
assertThat(telemetry.misclassifiedTestLines()).isEqualTo(1);
208244
}
209245

210246
@Test
@@ -222,8 +258,58 @@ def foo():
222258
var telemetry = collector.getTelemetry();
223259
assertThat(telemetry.totalMainFiles()).isEqualTo(1);
224260
assertThat(telemetry.misclassifiedTestFiles()).isEqualTo(1);
261+
assertThat(telemetry.totalLines()).isEqualTo(5);
262+
assertThat(telemetry.totalMainLines()).isEqualTo(5);
263+
assertThat(telemetry.testLines()).isZero();
264+
assertThat(telemetry.misclassifiedTestLines()).isEqualTo(5);
225265
}
226266

267+
@Test
268+
void lineCountAggregatesCorrectlyForDifferentFileSizes() {
269+
var smallFile = createContext("x = 1");
270+
var largerFile = createContext("""
271+
import os
272+
import sys
273+
import json
274+
275+
def foo():
276+
pass
277+
278+
def bar():
279+
pass
280+
""");
281+
282+
var collector = new TestFileTelemetryCollector();
283+
collector.collect(smallFile.rootTree(), InputFile.Type.MAIN);
284+
collector.collect(largerFile.rootTree(), InputFile.Type.MAIN);
227285

286+
var telemetry = collector.getTelemetry();
287+
assertThat(telemetry.totalMainLines()).isEqualTo(11);
288+
assertThat(telemetry.misclassifiedTestLines()).isZero();
289+
}
290+
291+
@Test
292+
void misclassifiedTestLinesOnlyCountsMisclassifiedFiles() {
293+
var misclassifiedFile = createContext("""
294+
import pytest
295+
296+
def test_something():
297+
assert True
298+
""");
299+
var regularFile = createContext("""
300+
import os
301+
302+
def helper():
303+
pass
304+
""");
305+
306+
var collector = new TestFileTelemetryCollector();
307+
collector.collect(misclassifiedFile.rootTree(), InputFile.Type.MAIN);
308+
collector.collect(regularFile.rootTree(), InputFile.Type.MAIN);
309+
310+
var telemetry = collector.getTelemetry();
311+
assertThat(telemetry.totalMainLines()).isEqualTo(10);
312+
assertThat(telemetry.misclassifiedTestLines()).isEqualTo(5);
313+
}
228314
}
229315

python-commons/src/test/java/org/sonar/plugins/python/telemetry/collectors/TestFileTelemetryTest.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,38 @@ void emptyTelemetry() {
2727
var empty = TestFileTelemetry.empty();
2828
assertThat(empty.totalMainFiles()).isZero();
2929
assertThat(empty.misclassifiedTestFiles()).isZero();
30+
assertThat(empty.totalLines()).isZero();
31+
assertThat(empty.totalMainLines()).isZero();
32+
assertThat(empty.testLines()).isZero();
33+
assertThat(empty.misclassifiedTestLines()).isZero();
3034
}
3135

3236
@Test
3337
void addTelemetry() {
34-
var telemetry1 = new TestFileTelemetry(10, 3);
35-
var telemetry2 = new TestFileTelemetry(5, 2);
38+
var telemetry1 = new TestFileTelemetry(10, 3, 800, 500, 300, 150);
39+
var telemetry2 = new TestFileTelemetry(5, 2, 350, 200, 150, 80);
3640

3741
var combined = telemetry1.add(telemetry2);
3842
assertThat(combined.totalMainFiles()).isEqualTo(15);
3943
assertThat(combined.misclassifiedTestFiles()).isEqualTo(5);
44+
assertThat(combined.totalLines()).isEqualTo(1150);
45+
assertThat(combined.totalMainLines()).isEqualTo(700);
46+
assertThat(combined.testLines()).isEqualTo(450);
47+
assertThat(combined.misclassifiedTestLines()).isEqualTo(230);
4048
}
4149

4250
@Test
4351
void addToEmpty() {
4452
var empty = TestFileTelemetry.empty();
45-
var telemetry = new TestFileTelemetry(7, 1);
53+
var telemetry = new TestFileTelemetry(7, 1, 500, 300, 200, 50);
4654

4755
var combined = empty.add(telemetry);
4856
assertThat(combined.totalMainFiles()).isEqualTo(7);
4957
assertThat(combined.misclassifiedTestFiles()).isEqualTo(1);
58+
assertThat(combined.totalLines()).isEqualTo(500);
59+
assertThat(combined.totalMainLines()).isEqualTo(300);
60+
assertThat(combined.testLines()).isEqualTo(200);
61+
assertThat(combined.misclassifiedTestLines()).isEqualTo(50);
5062
}
5163
}
5264

0 commit comments

Comments
 (0)