Skip to content

Commit 1860fd5

Browse files
authored
Identifying configure Rust unit tests attributes (#41)
* Highlight unit test lines * add property to identify test attributes * allow to override default test attributes
1 parent fb1e483 commit 1860fd5

File tree

6 files changed

+373
-66
lines changed

6 files changed

+373
-66
lines changed

DOC.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,20 @@ e.g
6868

6969
But [other coverage tools](https://vladfilippov.com/blog/rust-code-coverage-tools/) might work as well
7070

71+
## Highlighting unit tests
72+
By default, the plugin will highlight Rust unit tests for functions having attributes `#[test]` or `#[tokio::test]`
73+
You may configure different attributes with parameter `community.rust.unitttests.attributes`
74+
7175

7276
## Adding test measures
7377

7478
Optionally SonarQube can also display tests measures.
7579

76-
This Community Rust plugin doesn't run your tests or generate tests reports for you. That has to be done before analysis and provided in the form of reports.
80+
This Community Rust plugin doesn't run your tests or generate tests reports for you. That has to be done before analysis
81+
and provided in the form of reports.
7782

7883
Currently, only `junit report` formats are supported :
7984

80-
Insert a parameter `community.rust.test.reportPath` into you `sonar-project.properties` file. As an example, one of such tool
85+
Insert a parameter `community.rust.test.reportPath` into you `sonar-project.properties` file.
86+
As an example, one of such tool for Rust that converts `cargo test` report to `junit report` is [cargo2junit](https://crates.io/crates/cargo2junit).
8187

82-
for Rust than converts `cargo test` report to `junit report` is [cargo2junit](https://crates.io/crates/cargo2junit).

community-rust-plugin/src/main/java/org/elegoff/plugins/communityrust/CommunityRustPlugin.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public class CommunityRustPlugin implements Plugin {
4444
public static final String DEFAULT_LCOV_REPORT_PATHS = "lcov.info";
4545
public static final String COBERTURA_REPORT_PATHS = "community.rust.cobertura.reportPaths";
4646
public static final String DEFAULT_COBERTURA_REPORT_PATHS = "cobertura.xml";
47+
public static final String UNIT_TEST_ATTRIBUTES = "community.rust.unittests.attributes";
48+
public static final String TEST_AND_COVERAGE = "Test and Coverage";
49+
public static final String DEFAULT_UNIT_TEST_ATTRIBUTES="test,tokio::test";
4750

4851
@Override
4952
public void define(Context context) {
@@ -73,7 +76,7 @@ public void define(Context context) {
7376
.name("LCOV Files")
7477
.description("Paths (absolute or relative) to the files with LCOV data.")
7578
.onQualifiers(Qualifiers.PROJECT)
76-
.subCategory("Test and Coverage")
79+
.subCategory(TEST_AND_COVERAGE)
7780
.category("Rust")
7881
.multiValues(true)
7982
.build(),
@@ -84,12 +87,20 @@ public void define(Context context) {
8487
.name("LCOV Files")
8588
.description("Paths (absolute or relative) to the files with LCOV data.")
8689
.onQualifiers(Qualifiers.PROJECT)
87-
.subCategory("Test and Coverage")
90+
.subCategory(TEST_AND_COVERAGE)
8891
.category("Rust")
8992
.multiValues(true)
90-
.build()
91-
93+
.build(),
9294

95+
PropertyDefinition.builder(UNIT_TEST_ATTRIBUTES)
96+
.defaultValue(DEFAULT_UNIT_TEST_ATTRIBUTES)
97+
.name("Unit tests")
98+
.description("Comme separated list of Rust attributes for Unit Tests")
99+
.onQualifiers(Qualifiers.PROJECT)
100+
.subCategory(TEST_AND_COVERAGE)
101+
.category("Rust")
102+
.multiValues(true)
103+
.build()
93104
);
94105

95106

@@ -98,7 +109,7 @@ public void define(Context context) {
98109
.name("Path to xunit report(s)")
99110
.description("Path to the report of test execution, relative to project's root. Ant patterns are accepted. The reports have to conform to the junitreport XML format.")
100111
.category("Rust")
101-
.subCategory("Test and Coverage")
112+
.subCategory(TEST_AND_COVERAGE)
102113
.onQualifiers(Qualifiers.PROJECT)
103114
.defaultValue(XUnitSensor.DEFAULT_REPORT_PATH)
104115
.build(),

community-rust-plugin/src/main/java/org/elegoff/plugins/communityrust/RustTokensVisitor.java

Lines changed: 114 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
* Copyright (C) 2021 Eric Le Goff
44
* mailto:community-rust AT pm DOT me
55
* http://github.com/elegoff/sonar-rust
6-
*
6+
* <p>
77
* This program is free software; you can redistribute it and/or
88
* modify it under the terms of the GNU Lesser General Public
99
* License as published by the Free Software Foundation; either
1010
* version 3 of the License, or (at your option) any later version.
11-
*
11+
* <p>
1212
* This program is distributed in the hope that it will be useful,
1313
* but WITHOUT ANY WARRANTY; without even the implied warranty of
1414
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1515
* Lesser General Public License for more details.
16-
*
16+
* <p>
1717
* You should have received a copy of the GNU Lesser General Public License
1818
* along with this program; if not, write to the Free Software Foundation,
1919
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@@ -24,92 +24,150 @@
2424
import com.sonar.sslr.api.GenericTokenType;
2525
import com.sonar.sslr.api.Token;
2626
import com.sonar.sslr.api.Trivia;
27+
import org.apache.commons.lang.StringUtils;
2728
import org.sonar.api.batch.fs.InputFile;
2829
import org.sonar.api.batch.sensor.SensorContext;
2930
import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
3031
import org.sonar.api.batch.sensor.highlighting.TypeOfText;
32+
import org.sonar.api.config.Configuration;
3133
import org.sonar.rust.RustVisitorContext;
3234
import org.sonar.rust.api.RustKeyword;
3335
import org.sonar.rust.api.RustTokenType;
3436
import org.sonar.sslr.parser.LexerlessGrammar;
3537
import org.sonar.sslr.parser.ParserAdapter;
3638
import org.sonarsource.analyzer.commons.TokenLocation;
3739

38-
import java.util.Arrays;
39-
import java.util.HashSet;
40-
import java.util.Locale;
41-
import java.util.Set;
40+
import java.util.*;
41+
import java.util.stream.Collectors;
42+
import java.util.stream.IntStream;
4243

43-
public class RustTokensVisitor{
44+
public class RustTokensVisitor {
4445

4546

46-
private final Set<String> keywords = new HashSet<>(Arrays.asList(RustKeyword.keywordValues()));
47-
private final SensorContext context;
48-
private final ParserAdapter<LexerlessGrammar> lexer;
47+
private final Set<String> keywords = new HashSet<>(Arrays.asList(RustKeyword.keywordValues()));
48+
private final SensorContext context;
49+
private final ParserAdapter<LexerlessGrammar> lexer;
4950

50-
public RustTokensVisitor(SensorContext context, ParserAdapter<LexerlessGrammar> lexer) {
51-
this.context = context;
52-
this.lexer = lexer;
53-
}
51+
public RustTokensVisitor(SensorContext context, ParserAdapter<LexerlessGrammar> lexer) {
52+
this.context = context;
53+
this.lexer = lexer;
54+
}
5455

55-
private static String getTokenImage(Token token) {
56-
if (token.getType().equals(RustTokenType.CHARACTER_LITERAL)) {
57-
return RustTokenType.CHARACTER_LITERAL.getValue();
58-
}
59-
return token.getValue().toLowerCase(Locale.ENGLISH);
56+
private static String getTokenImage(Token token) {
57+
if (token.getType().equals(RustTokenType.CHARACTER_LITERAL)) {
58+
return RustTokenType.CHARACTER_LITERAL.getValue();
6059
}
60+
return token.getValue().toLowerCase(Locale.ENGLISH);
61+
}
6162

62-
private static void highlight(NewHighlighting highlighting, TokenLocation tokenLocation, TypeOfText typeOfText) {
63-
highlighting.highlight(tokenLocation.startLine(), tokenLocation.startLineOffset(), tokenLocation.endLine(), tokenLocation.endLineOffset(), typeOfText);
64-
}
63+
private static void highlight(NewHighlighting highlighting, TokenLocation tokenLocation, TypeOfText typeOfText) {
64+
highlighting.highlight(tokenLocation.startLine(), tokenLocation.startLineOffset(), tokenLocation.endLine(), tokenLocation.endLineOffset(), typeOfText);
65+
}
6566

66-
private static TokenLocation tokenLocation(Token token) {
67-
return new TokenLocation(token.getLine(), token.getColumn(), token.getOriginalValue());
68-
}
67+
private static TokenLocation tokenLocation(Token token) {
68+
return new TokenLocation(token.getLine(), token.getColumn(), token.getOriginalValue());
69+
}
6970

70-
public void scanFile(InputFile inputFile, RustVisitorContext visitorContext) {
71-
var highlighting = context.newHighlighting();
72-
highlighting.onFile(inputFile);
71+
public void scanFile(InputFile inputFile, RustVisitorContext visitorContext) {
72+
var highlighting = context.newHighlighting();
73+
highlighting.onFile(inputFile);
7374

74-
var cpdTokens = context.newCpdTokens();
75-
cpdTokens.onFile(inputFile);
75+
var cpdTokens = context.newCpdTokens();
76+
cpdTokens.onFile(inputFile);
7677

77-
for (Token token : lexer.parse(visitorContext.file().content()).getTokens()) {
78-
final String tokenImage = getTokenImage(token);
79-
final var tokenLocation = tokenLocation(token);
78+
List<Token> parsedTokens = lexer.parse(visitorContext.file().content()).getTokens();
79+
Set<Token> unitTestTokens = identifyUnitTestTokens(parsedTokens);
8080

81-
if (token.getType().equals(RustTokenType.CHARACTER_LITERAL)
82-
||token.getType().equals(RustTokenType.STRING_LITERAL)
83-
||token.getType().equals(RustTokenType.RAW_STRING_LITERAL)
84-
||token.getType().equals(RustTokenType.RAW_BYTE_STRING_LITERAL)
81+
for (Token token : parsedTokens) {
82+
final String tokenImage = getTokenImage(token);
83+
final var tokenLocation = tokenLocation(token);
8584

86-
) {
87-
highlight(highlighting, tokenLocation, TypeOfText.STRING);
85+
if (token.getType().equals(RustTokenType.CHARACTER_LITERAL)
86+
|| token.getType().equals(RustTokenType.STRING_LITERAL)
87+
|| token.getType().equals(RustTokenType.RAW_STRING_LITERAL)
88+
|| token.getType().equals(RustTokenType.RAW_BYTE_STRING_LITERAL)
8889

89-
} else if (keywords.contains(tokenImage)) {
90-
highlight(highlighting, tokenLocation, TypeOfText.KEYWORD);
91-
}
90+
) {
91+
highlight(highlighting, tokenLocation, TypeOfText.STRING);
9292

93-
if (token.getType().equals(RustTokenType.FLOAT_LITERAL)
94-
||token.getType().equals(RustTokenType.BOOLEAN_LITERAL)
95-
||token.getType().equals(RustTokenType.INTEGER_LITERAL)
93+
} else if (keywords.contains(tokenImage)) {
94+
highlight(highlighting, tokenLocation, TypeOfText.KEYWORD);
95+
}
9696

97-
) {
98-
highlight(highlighting, tokenLocation, TypeOfText.CONSTANT);
97+
if (token.getType().equals(RustTokenType.FLOAT_LITERAL)
98+
|| token.getType().equals(RustTokenType.BOOLEAN_LITERAL)
99+
|| token.getType().equals(RustTokenType.INTEGER_LITERAL)) {
100+
highlight(highlighting, tokenLocation, TypeOfText.CONSTANT);
101+
}
99102

100-
}
103+
for (Trivia trivia : token.getTrivia()) {
104+
highlight(highlighting, tokenLocation(trivia.getToken()), TypeOfText.COMMENT);
105+
}
106+
107+
if (unitTestTokens.contains(token)) {
108+
highlight(highlighting, tokenLocation, TypeOfText.ANNOTATION
109+
);
110+
}
101111

102-
for (Trivia trivia : token.getTrivia()) {
103-
highlight(highlighting, tokenLocation(trivia.getToken()), TypeOfText.COMMENT);
112+
if (!GenericTokenType.EOF.equals(token.getType())) {
113+
cpdTokens.addToken(tokenLocation.startLine(), tokenLocation.startLineOffset(), tokenLocation.endLine(), tokenLocation.endLineOffset(), tokenImage);
114+
}
115+
}
116+
117+
highlighting.save();
118+
cpdTokens.save();
119+
}
120+
121+
private Set<Token> identifyUnitTestTokens(List<Token> parsedTokens) {
122+
Set<Token> testTokens = new HashSet<>();
123+
Set<String> unitTestsAttributes = getUnitTestAttributes();
124+
int i = 0;
125+
while (i < parsedTokens.size()) {
126+
if ("#".equals(getTokenImage(parsedTokens.get(i))) && ("[".equals(getTokenImage(parsedTokens.get(i + 1)))) && (unitTestsAttributes.contains(getTokenImage(parsedTokens.get(i + 2)))) && ("]".equals(getTokenImage(parsedTokens.get(i + 3)))) && ("fn".equals(getTokenImage(parsedTokens.get(i + 4))))) {
127+
int j = i + 5;
128+
//lookup for opening bracket
129+
while (!"{".equals(getTokenImage(parsedTokens.get(j)))) {
130+
j++;
104131
}
105132

106-
if (!GenericTokenType.EOF.equals(token.getType())) {
107-
cpdTokens.addToken(tokenLocation.startLine(), tokenLocation.startLineOffset(), tokenLocation.endLine(), tokenLocation.endLineOffset(), tokenImage);
133+
int cptOpeningBracket = 1;
134+
//lookup for outer closing bracket (end of test function position)
135+
while (cptOpeningBracket > 0) {
136+
j++;
137+
String tokenImage = getTokenImage(parsedTokens.get(j));
138+
if ("{".equals(tokenImage)) {
139+
cptOpeningBracket++;
140+
} else if ("}".equals(tokenImage)) {
141+
cptOpeningBracket--;
142+
}
143+
108144
}
109-
}
110145

111-
highlighting.save();
112-
cpdTokens.save();
146+
//all tokens constituting a test function are added to the set
147+
IntStream.rangeClosed(i, j).mapToObj(parsedTokens::get).forEach(testTokens::add);
148+
}
149+
i++;
150+
}
151+
return testTokens;
152+
}
153+
154+
private Set<String> getUnitTestAttributes() {
155+
Configuration config = context.config();
156+
String[] attrs = filterEmptyStrings(config.getStringArray(CommunityRustPlugin.UNIT_TEST_ATTRIBUTES));
157+
if (attrs.length == 0) {
158+
attrs = StringUtils.split(CommunityRustPlugin.DEFAULT_UNIT_TEST_ATTRIBUTES, ",");
159+
}
160+
return Arrays.stream(attrs).collect(Collectors.toSet());
161+
}
162+
163+
private String[] filterEmptyStrings(String[] stringArray) {
164+
List<String> nonEmptyStrings = new ArrayList<>();
165+
for (String string : stringArray) {
166+
if (StringUtils.isNotBlank(string.trim())) {
167+
nonEmptyStrings.add(string.trim());
168+
}
113169
}
170+
return nonEmptyStrings.toArray(new String[nonEmptyStrings.size()]);
171+
}
114172

115173
}

community-rust-plugin/src/test/java/org/elegoff/plugins/communityrust/CommunityRustPluginTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ public class CommunityRustPluginTest extends TestCase {
3939
public void testGetExtensions() {
4040
Version v79 = Version.create(7, 9);
4141
SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(v79, SonarQubeSide.SERVER, SonarEdition.DEVELOPER);
42-
assertThat(extensions(runtime)).hasSize(15);
42+
assertThat(extensions(runtime)).hasSize(16);
4343
assertThat(extensions(runtime)).contains(ClippyRulesDefinition.class);
44-
assertThat(extensions(SonarRuntimeImpl.forSonarLint(v79))).hasSize(15);
44+
assertThat(extensions(SonarRuntimeImpl.forSonarLint(v79))).hasSize(16);
4545
}
4646

4747
private static List extensions(SonarRuntime runtime) {

community-rust-plugin/src/test/java/org/elegoff/plugins/communityrust/RustSensorTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ public void canParse() throws IOException {
114114
Assertions.assertThat(tester.allAnalysisErrors()).isEmpty();
115115
}
116116

117+
@Test
118+
public void checkDuplication() throws IOException {
119+
DefaultInputFile inputFile = executeSensorOnSingleFile("sensor/cpd.rs");
120+
assertEquals(212, tester.cpdTokens(inputFile.key()).size());
121+
verify(fileLinesContext).save();
122+
assertEquals(Collections.singletonList(TypeOfText.ANNOTATION), tester.highlightingTypeAt(inputFile.key(), 5, 5));
123+
Assertions.assertThat(tester.allAnalysisErrors()).isEmpty();
124+
}
125+
117126

118127

119128
private DefaultInputFile executeSensorOnSingleFile(String fileName) throws IOException {

0 commit comments

Comments
 (0)