Browse Source

SONAR-6931 Speed up issues mode by scanning only changed files

tags/5.3-RC1
Duarte Meneses 8 years ago
parent
commit
7f652d4d5e

+ 100
- 2
it/it-tests/src/test/java/it/analysis/IssuesModeTest.java View File

@@ -5,6 +5,8 @@
*/
package it.analysis;

import org.apache.commons.io.FileUtils;
import org.sonar.wsclient.issue.IssueClient;
import com.google.common.collect.Maps;
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.BuildFailureException;
@@ -15,7 +17,10 @@ import com.sonar.orchestrator.config.FileSystem;
import com.sonar.orchestrator.locator.FileLocation;
import com.sonar.orchestrator.version.Version;
import it.Category3Suite;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -24,6 +29,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.lang.ObjectUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
@@ -41,7 +47,6 @@ import org.sonar.wsclient.services.Resource;
import org.sonar.wsclient.services.ResourceQuery;
import org.sonar.wsclient.user.UserParameters;
import util.ItUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

@@ -89,7 +94,7 @@ public class IssuesModeTest {
public void project_key_with_slash() throws IOException {
restoreProfile("one-issue-per-line.xml");
setDefaultQualityProfile("xoo", "one-issue-per-line");
SonarRunner runner = configureRunner("shared/xoo-sample");
runner.setProjectKey("sample/project");
runner.setProperty("sonar.analysis.mode", "issues");
@@ -97,6 +102,99 @@ public class IssuesModeTest {
assertThat(ItUtils.countIssuesInJsonReport(result, true)).isEqualTo(17);
}

// SONAR-6931
@Test
public void only_scan_changed_files_qps() throws IOException {
restoreProfile("one-issue-per-line.xml");
orchestrator.getServer().provisionProject("sample", "xoo-sample");
orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");

SonarRunner runner = configureRunner("shared/xoo-sample", "sonar.verbose", "true");
BuildResult result = orchestrator.executeBuild(runner);
List<Issue> serverIssues = ItUtils.getAllServerIssues(orchestrator);
for (Issue i : serverIssues) {
assertThat(i.status()).isEqualTo("OPEN");
}
assertThat(serverIssues).hasSize(17);
// change quality profile
restoreProfile("with-many-rules.xml");
orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "with-many-rules");
// do it again, scanning nothing (all files should be unchanged)
runner = configureRunnerIssues("shared/xoo-sample",
"sonar.verbose", "true",
"sonar.scanChangedFilesOnly", "true");
result = orchestrator.executeBuild(runner);
assertThat(result.getLogs()).contains("Scanning only changed files");
assertThat(result.getLogs()).contains("'One Issue Per Line' skipped because there is no related file in current project");
ItUtils.assertIssuesInJsonReport(result, 0, 0, 17);
}
// SONAR-6931
@Test
public void only_scan_changed_files_transitions() throws IOException {
restoreProfile("one-issue-per-line.xml");
orchestrator.getServer().provisionProject("sample", "xoo-sample");
orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");

SonarRunner runner = configureRunner("shared/xoo-sample", "sonar.verbose", "true");
BuildResult result = orchestrator.executeBuild(runner);
List<Issue> serverIssues = ItUtils.getAllServerIssues(orchestrator);
for (Issue i : serverIssues) {
assertThat(i.status()).isEqualTo("OPEN");
}
assertThat(serverIssues).hasSize(17);
// resolve 2 issues
IssueClient issueClient = orchestrator.getServer().wsClient("admin", "admin").issueClient();
issueClient.doTransition(serverIssues.get(0).key(), "wontfix");
issueClient.doTransition(serverIssues.get(1).key(), "wontfix");

// do it again, scanning nothing (all files should be unchanged)
runner = configureRunnerIssues("shared/xoo-sample",
"sonar.verbose", "true",
"sonar.scanChangedFilesOnly", "true");
result = orchestrator.executeBuild(runner);
assertThat(result.getLogs()).contains("Scanning only changed files");
assertThat(result.getLogs()).contains("'One Issue Per Line' skipped because there is no related file in current project");
ItUtils.assertIssuesInJsonReport(result, 0, 0, 15);
}
// SONAR-6931
@Test
public void only_scan_changed_files_on_change() throws IOException {
restoreProfile("one-issue-per-line.xml");
orchestrator.getServer().provisionProject("sample", "xoo-sample");
orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");

SonarRunner runner = configureRunner("shared/xoo-sample", "sonar.verbose", "true");
BuildResult result = orchestrator.executeBuild(runner);
// change QP
restoreProfile("with-many-rules.xml");
orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "with-many-rules");
// now change file hash in a temporary location
File tmpProjectDir = temp.newFolder();
FileUtils.copyDirectory(ItUtils.projectDir("shared/xoo-sample"), tmpProjectDir);
File srcFile = new File(tmpProjectDir, "src/main/xoo/sample/Sample.xoo");
FileUtils.write(srcFile, "\n", StandardCharsets.UTF_8, true);

// scan again, with different QP
runner = SonarRunner.create(tmpProjectDir,
"sonar.working.directory", temp.newFolder().getAbsolutePath(),
"sonar.analysis.mode", "issues",
"sonar.report.export.path", "sonar-report.json",
"sonar.userHome", temp.newFolder().getAbsolutePath(),
"sonar.verbose", "true",
"sonar.scanChangedFilesOnly", "true");
result = orchestrator.executeBuild(runner);
assertThat(result.getLogs()).contains("Scanning only changed files");
assertThat(result.getLogs()).doesNotContain("'One Issue Per Line' skipped because there is no related file in current project");
ItUtils.assertIssuesInJsonReport(result, 3, 0, 17);
}
@Test
public void non_associated_mode() throws IOException {
restoreProfile("one-issue-per-line.xml");

+ 39
- 3
it/it-tests/src/test/java/util/ItUtils.java View File

@@ -4,6 +4,10 @@ package util;/*
* mailto:contact AT sonarsource DOT com
*/

import org.sonar.wsclient.issue.Issue;

import org.sonar.wsclient.issue.IssueQuery;
import org.sonar.wsclient.issue.IssueClient;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
@@ -12,14 +16,18 @@ import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.BuildResult;
import com.sonar.orchestrator.build.SonarRunner;
import com.sonar.orchestrator.locator.FileLocation;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.apache.commons.io.FileUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
@@ -27,7 +35,6 @@ import org.json.simple.JSONValue;
import org.sonar.wsclient.base.HttpException;
import org.sonar.wsclient.services.PropertyDeleteQuery;
import org.sonar.wsclient.services.PropertyUpdateQuery;

import static com.google.common.collect.FluentIterable.from;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
@@ -63,6 +70,12 @@ public class ItUtils {
throw new IllegalStateException("XOO plugin is not built");
}

public static List<Issue> getAllServerIssues(Orchestrator orchestrator) {
IssueClient issueClient = orchestrator.getServer().wsClient().issueClient();
return issueClient.find(IssueQuery.create()).list();

}

/**
* Locate the directory of sample project
*
@@ -137,6 +150,29 @@ public class ItUtils {
return count;
}

public static void assertIssuesInJsonReport(BuildResult result, int newIssues, int resolvedIssues, int existingIssues) {
JSONObject obj = getJSONReport(result);
JSONArray issues = (JSONArray) obj.get("issues");
int countNew = 0;
int countResolved = 0;
int countExisting = 0;

for (Object issue : issues) {
JSONObject jsonIssue = (JSONObject) issue;

if ((Boolean) jsonIssue.get("isNew")) {
countNew++;
} else if (jsonIssue.get("resolution") != null) {
countResolved++;
} else {
countExisting++;
}
}
assertThat(countNew).isEqualTo(newIssues);
assertThat(countResolved).isEqualTo(resolvedIssues);
assertThat(countExisting).isEqualTo(existingIssues);
}

public static SonarRunner runVerboseProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, String... properties) {
return runProjectAnalysis(orchestrator, projectRelativePath, true, properties);
}
@@ -164,7 +200,7 @@ public class ItUtils {
}
}

public static void resetPeriods(Orchestrator orchestrator){
public static void resetPeriods(Orchestrator orchestrator) {
setServerProperty(orchestrator, "sonar.timemachine.period1", null);
setServerProperty(orchestrator, "sonar.timemachine.period2", null);
setServerProperty(orchestrator, "sonar.timemachine.period3", null);
@@ -204,4 +240,4 @@ public class ItUtils {
}
}

}
}

+ 1
- 1
sonar-batch/src/main/java/org/sonar/batch/DefaultProjectTree.java View File

@@ -33,7 +33,7 @@ import org.sonar.batch.scan.ImmutableProjectReactor;
public class DefaultProjectTree implements Startable {

private final ProjectConfigurator configurator;
private ImmutableProjectReactor projectReactor;
private final ImmutableProjectReactor projectReactor;

private List<Project> projects;
private Map<ProjectDefinition, Project> projectsByDef;

+ 11
- 0
sonar-batch/src/main/java/org/sonar/batch/analysis/DefaultAnalysisMode.java View File

@@ -36,9 +36,11 @@ import java.util.Map;
public class DefaultAnalysisMode extends AbstractAnalysisMode implements AnalysisMode {

private static final Logger LOG = LoggerFactory.getLogger(DefaultAnalysisMode.class);
private static final String KEY_ONLY_ANALYZE_CHANGED = "sonar.scanChangedFilesOnly";

private boolean mediumTestMode;
private boolean notAssociated;
private boolean onlyChanged;

public DefaultAnalysisMode(GlobalProperties globalProps, AnalysisProperties props) {
init(globalProps.properties(), props.properties());
@@ -52,6 +54,10 @@ public class DefaultAnalysisMode extends AbstractAnalysisMode implements Analysi
return notAssociated;
}

public boolean onlyAnalyzeChanged() {
return onlyChanged;
}

private void init(Map<String, String> globalProps, Map<String, String> analysisProps) {
// make sure analysis is consistent with global properties
boolean globalPreview = isIssues(globalProps);
@@ -70,6 +76,8 @@ public class DefaultAnalysisMode extends AbstractAnalysisMode implements Analysi
issues = CoreProperties.ANALYSIS_MODE_ISSUES.equals(mode) || CoreProperties.ANALYSIS_MODE_PREVIEW.equals(mode);
mediumTestMode = "true".equals(getPropertyWithFallback(analysisProps, globalProps, FakePluginInstaller.MEDIUM_TEST_ENABLED));
notAssociated = issues && rootProjectKeyMissing(analysisProps);
String onlyChangedStr = getPropertyWithFallback(analysisProps, globalProps, KEY_ONLY_ANALYZE_CHANGED);
onlyChanged = issues && "true".equals(onlyChangedStr);
}

public void printMode() {
@@ -86,6 +94,9 @@ public class DefaultAnalysisMode extends AbstractAnalysisMode implements Analysi
if (notAssociated) {
LOG.info("Local analysis");
}
if (onlyChanged) {
LOG.info("Scanning only changed files");
}
}

private static String getPropertyWithFallback(Map<String, String> props1, Map<String, String> props2, String key) {

+ 47
- 9
sonar-batch/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java View File

@@ -19,14 +19,20 @@
*/
package org.sonar.batch.issue.tracking;

import org.sonar.api.batch.fs.InputFile.Status;

import org.sonar.batch.analysis.DefaultAnalysisMode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.annotation.CheckForNull;

import org.sonar.api.batch.BatchSide;
import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.rule.ActiveRule;
@@ -54,15 +60,18 @@ public class LocalIssueTracking {
private final ActiveRules activeRules;
private final ServerIssueRepository serverIssueRepository;
private final Date analysisDate;
private final DefaultAnalysisMode mode;

private boolean hasServerAnalysis;

public LocalIssueTracking(BatchComponentCache resourceCache, IssueTracking tracking, ServerLineHashesLoader lastLineHashes, IssueUpdater updater,
ActiveRules activeRules, ServerIssueRepository serverIssueRepository, ProjectRepositories projectRepositories, ReportPublisher reportPublisher) {
ActiveRules activeRules, ServerIssueRepository serverIssueRepository, ProjectRepositories projectRepositories, ReportPublisher reportPublisher,
DefaultAnalysisMode mode) {
this.tracking = tracking;
this.lastLineHashes = lastLineHashes;
this.updater = updater;
this.serverIssueRepository = serverIssueRepository;
this.mode = mode;
this.analysisDate = ((Project) resourceCache.getRoot().resource()).getAnalysisDate();
this.changeContext = IssueChangeContext.createScan(analysisDate);
this.activeRules = activeRules;
@@ -78,18 +87,23 @@ public class LocalIssueTracking {
public List<DefaultIssue> trackIssues(BatchComponent component, Set<BatchReport.Issue> rawIssues) {
List<DefaultIssue> trackedIssues = Lists.newArrayList();
if (hasServerAnalysis) {

// all the issues that are not closed in db before starting this module scan, including manual issues
Collection<ServerIssue> serverIssues = loadServerIssues(component);

SourceHashHolder sourceHashHolder = loadSourceHashes(component);
if (shouldCopyServerIssues(component)) {
// raw issues should be empty, we just need to deal with server issues (SONAR-6931)
copyServerIssues(serverIssues, trackedIssues);
} else {

IssueTrackingResult trackingResult = tracking.track(sourceHashHolder, serverIssues, rawIssues);
SourceHashHolder sourceHashHolder = loadSourceHashes(component);

// unmatched from server = issues that have been resolved + issues on disabled/removed rules + manual issues
addUnmatchedFromServer(trackingResult.unmatched(), sourceHashHolder, trackedIssues);
IssueTrackingResult trackingResult = tracking.track(sourceHashHolder, serverIssues, rawIssues);

mergeMatched(component, trackingResult, trackedIssues, rawIssues);
// unmatched from server = issues that have been resolved + issues on disabled/removed rules + manual issues
addUnmatchedFromServer(trackingResult.unmatched(), sourceHashHolder, trackedIssues);

mergeMatched(component, trackingResult, trackedIssues, rawIssues);
}
}

if (hasServerAnalysis && ResourceUtils.isRootProject(component.resource())) {
@@ -100,8 +114,33 @@ public class LocalIssueTracking {
return trackedIssues;
}

private boolean shouldCopyServerIssues(BatchComponent component) {
if (mode.onlyAnalyzeChanged() && component.isFile()) {
DefaultInputFile inputFile = (DefaultInputFile) component.inputComponent();
if (inputFile.status() == Status.SAME) {
return true;
}
}
return false;
}

private void copyServerIssues(Collection<ServerIssue> serverIssues, List<DefaultIssue> trackedIssues) {
for (ServerIssue serverIssue : serverIssues) {
org.sonar.batch.protocol.input.BatchInput.ServerIssue unmatchedPreviousIssue = ((ServerIssueFromWs) serverIssue).getDto();
DefaultIssue unmatched = toUnmatchedIssue(unmatchedPreviousIssue);

ActiveRule activeRule = activeRules.find(unmatched.ruleKey());
unmatched.setNew(false);

boolean isRemovedRule = activeRule == null;
unmatched.setBeingClosed(isRemovedRule);
unmatched.setOnDisabledRule(isRemovedRule);
trackedIssues.add(unmatched);
}
}

private DefaultIssue toTracked(BatchComponent component, BatchReport.Issue rawIssue) {
DefaultIssue trackedIssue = new org.sonar.core.issue.DefaultIssueBuilder()
return new org.sonar.core.issue.DefaultIssueBuilder()
.componentKey(component.key())
.projectKey("unused")
.ruleKey(RuleKey.of(rawIssue.getRuleRepository(), rawIssue.getRuleKey()))
@@ -110,7 +149,6 @@ public class LocalIssueTracking {
.message(rawIssue.hasMsg() ? rawIssue.getMsg() : null)
.severity(rawIssue.getSeverity().name())
.build();
return trackedIssue;
}

@CheckForNull

+ 1
- 1
sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/ComponentIndexer.java View File

@@ -53,7 +53,7 @@ public class ComponentIndexer {
public void execute(DefaultModuleFileSystem fs) {
module.setBaseDir(fs.baseDir());

for (InputFile inputFile : fs.inputFiles(fs.predicates().all())) {
for (InputFile inputFile : fs.inputFiles()) {
String languageKey = inputFile.language();
boolean unitTest = InputFile.Type.TEST == inputFile.type();
Resource sonarFile = File.create(inputFile.relativePath(), languages.get(languageKey), unitTest);

+ 18
- 2
sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystem.java View File

@@ -19,19 +19,25 @@
*/
package org.sonar.batch.scan.filesystem;

import org.sonar.api.batch.fs.InputFile.Status;

import org.sonar.batch.analysis.DefaultAnalysisMode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import org.apache.commons.lang.StringUtils;
import org.sonar.api.CoreProperties;
import org.sonar.api.batch.fs.FilePredicate;
@@ -59,7 +65,7 @@ public class DefaultModuleFileSystem extends DefaultFileSystem implements Module
private boolean initialized;

public DefaultModuleFileSystem(ModuleInputFileCache moduleInputFileCache, Project project,
Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer) {
Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer, DefaultAnalysisMode mode) {
super(initializer.baseDir(), moduleInputFileCache);
this.componentIndexer = componentIndexer;
this.moduleKey = project.getKey();
@@ -70,11 +76,16 @@ public class DefaultModuleFileSystem extends DefaultFileSystem implements Module
this.sourceDirsOrFiles = initializer.sources();
this.testDirsOrFiles = initializer.tests();
this.binaryDirs = initializer.binaryDirs();

// filter the files that sensors have access to (SONAR-6931)
if (mode.onlyAnalyzeChanged()) {
setDefaultPredicate(predicates.not(predicates.hasStatus(Status.SAME)));
}
}

@VisibleForTesting
public DefaultModuleFileSystem(Project project,
Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer) {
Settings settings, FileIndexer indexer, ModuleFileSystemInitializer initializer, ComponentIndexer componentIndexer, DefaultAnalysisMode mode) {
super(initializer.baseDir().toPath());
this.componentIndexer = componentIndexer;
this.moduleKey = project.getKey();
@@ -85,6 +96,11 @@ public class DefaultModuleFileSystem extends DefaultFileSystem implements Module
this.sourceDirsOrFiles = initializer.sources();
this.testDirsOrFiles = initializer.tests();
this.binaryDirs = initializer.binaryDirs();
// filter the files sensors have access to
if (mode.onlyAnalyzeChanged()) {
setDefaultPredicate(predicates.not(predicates.hasStatus(Status.SAME)));
}
}

public boolean isInitialized() {

+ 18
- 55
sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/InputPathCache.java View File

@@ -19,19 +19,14 @@
*/
package org.sonar.batch.scan.filesystem;

import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.annotation.CheckForNull;
import com.google.common.collect.Table;
import com.google.common.collect.TreeBasedTable;
import org.sonar.api.batch.BatchSide;
import org.sonar.api.batch.fs.InputDir;
import org.sonar.api.batch.fs.InputFile;

import javax.annotation.CheckForNull;

/**
* Cache of all files and dirs. This cache is shared amongst all project modules. Inclusion and
* exclusion patterns are already applied.
@@ -39,91 +34,59 @@ import org.sonar.api.batch.fs.InputFile;
@BatchSide
public class InputPathCache {

private final Map<String, SortedMap<String, InputFile>> inputFileCache = new LinkedHashMap<>();
private final Map<String, SortedMap<String, InputDir>> inputDirCache = new LinkedHashMap<>();
private final Table<String, String, InputFile> inputFileCache = TreeBasedTable.create();
private final Table<String, String, InputDir> inputDirCache = TreeBasedTable.create();

public Iterable<InputFile> allFiles() {
return Iterables.concat(Iterables.transform(inputFileCache.values(), new Function<Map<String, InputFile>, Collection<InputFile>>() {
@Override
public Collection<InputFile> apply(Map<String, InputFile> input) {
return input.values();
}
}));
return inputFileCache.values();
}

public Iterable<InputDir> allDirs() {
return Iterables.concat(Iterables.transform(inputDirCache.values(), new Function<Map<String, InputDir>, Collection<InputDir>>() {
@Override
public Collection<InputDir> apply(Map<String, InputDir> input) {
return input.values();
}
}));
return inputDirCache.values();
}

public Iterable<InputFile> filesByModule(String moduleKey) {
if (inputFileCache.containsKey(moduleKey)) {
return inputFileCache.get(moduleKey).values();
}
return Collections.emptyList();
return inputFileCache.row(moduleKey).values();
}

public Iterable<InputDir> dirsByModule(String moduleKey) {
if (inputDirCache.containsKey(moduleKey)) {
return inputDirCache.get(moduleKey).values();
}
return Collections.emptyList();
return inputDirCache.row(moduleKey).values();
}

public InputPathCache removeModule(String moduleKey) {
inputFileCache.remove(moduleKey);
inputDirCache.remove(moduleKey);
inputFileCache.row(moduleKey).clear();
inputDirCache.row(moduleKey).clear();
return this;
}

public InputPathCache remove(String moduleKey, InputFile inputFile) {
if (inputFileCache.containsKey(moduleKey)) {
inputFileCache.get(moduleKey).remove(inputFile.relativePath());
}
inputFileCache.remove(moduleKey, inputFile.relativePath());
return this;
}

public InputPathCache remove(String moduleKey, InputDir inputDir) {
if (inputDirCache.containsKey(moduleKey)) {
inputDirCache.get(moduleKey).remove(inputDir.relativePath());
}
inputDirCache.remove(moduleKey, inputDir.relativePath());
return this;
}

public InputPathCache put(String moduleKey, InputFile inputFile) {
if (!inputFileCache.containsKey(moduleKey)) {
inputFileCache.put(moduleKey, new TreeMap<String, InputFile>());
}
inputFileCache.get(moduleKey).put(inputFile.relativePath(), inputFile);
inputFileCache.put(moduleKey, inputFile.relativePath(), inputFile);
return this;
}

public InputPathCache put(String moduleKey, InputDir inputDir) {
if (!inputDirCache.containsKey(moduleKey)) {
inputDirCache.put(moduleKey, new TreeMap<String, InputDir>());
}
inputDirCache.get(moduleKey).put(inputDir.relativePath(), inputDir);
inputDirCache.put(moduleKey, inputDir.relativePath(), inputDir);
return this;
}

@CheckForNull
public InputFile getFile(String moduleKey, String relativePath) {
if (inputFileCache.containsKey(moduleKey)) {
return inputFileCache.get(moduleKey).get(relativePath);
}
return null;
return inputFileCache.get(moduleKey, relativePath);
}

@CheckForNull
public InputDir getDir(String moduleKey, String relativePath) {
if (inputDirCache.containsKey(moduleKey)) {
return inputDirCache.get(moduleKey).get(relativePath);
}
return null;
return inputDirCache.get(moduleKey, relativePath);
}

}

+ 21
- 1
sonar-batch/src/test/java/org/sonar/batch/analysis/DefaultAnalysisModeTest.java View File

@@ -20,7 +20,6 @@
package org.sonar.batch.analysis;

import org.junit.Rule;

import org.junit.rules.ExpectedException;
import org.sonar.batch.analysis.DefaultAnalysisMode;
import org.sonar.batch.analysis.AnalysisProperties;
@@ -84,10 +83,30 @@ public class DefaultAnalysisModeTest {
assertThat(mode.isPreview()).isFalse();
}

@Test
public void only_scan_changed() {
Map<String, String> props = new HashMap<>();
props.put(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES);
GlobalProperties globalProps = new GlobalProperties(props);

props.put("sonar.scanChangedFilesOnly", "true");
AnalysisProperties analysisProps = new AnalysisProperties(props);

DefaultAnalysisMode mode = new DefaultAnalysisMode(globalProps, analysisProps);
assertThat(mode.onlyAnalyzeChanged()).isTrue();

props.put(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_PUBLISH);
analysisProps = new AnalysisProperties(props);

mode = new DefaultAnalysisMode(globalProps, analysisProps);
assertThat(mode.onlyAnalyzeChanged()).isFalse();
}

@Test
public void default_publish_mode() {
DefaultAnalysisMode mode = createMode(null);
assertThat(mode.isPublish()).isTrue();
assertThat(mode.onlyAnalyzeChanged()).isFalse();
}

@Test
@@ -95,6 +114,7 @@ public class DefaultAnalysisModeTest {
DefaultAnalysisMode mode = createMode(CoreProperties.ANALYSIS_MODE_ISSUES);

assertThat(mode.isIssues()).isTrue();
assertThat(mode.onlyAnalyzeChanged()).isFalse();
}

private static DefaultAnalysisMode createMode(@Nullable String mode) {

+ 186
- 0
sonar-batch/src/test/java/org/sonar/batch/mediumtest/issuesmode/ScanOnlyChangedTest.java View File

@@ -0,0 +1,186 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.batch.mediumtest.issuesmode;

import org.assertj.core.api.Condition;

import com.google.common.io.Resources;
import org.sonar.batch.repository.FileData;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.junit.After;
import org.junit.Before;
import org.sonar.api.CoreProperties;
import org.sonar.batch.mediumtest.BatchMediumTester;
import org.sonar.batch.protocol.Constants.Severity;
import org.sonar.batch.protocol.input.BatchInput.ServerIssue;
import org.sonar.xoo.XooPlugin;
import org.sonar.xoo.rule.XooRulesDefinition;
import org.junit.rules.TemporaryFolder;
import org.sonar.api.utils.log.LogTester;
import org.junit.Test;
import org.sonar.api.issue.Issue;
import org.sonar.batch.mediumtest.TaskResult;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import static org.assertj.core.api.Assertions.assertThat;

public class ScanOnlyChangedTest {
@org.junit.Rule
public TemporaryFolder temp = new TemporaryFolder();

@org.junit.Rule
public LogTester logTester = new LogTester();

private static SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");

private BatchMediumTester tester;

private static Long date(String date) {
try {
return sdf.parse(date).getTime();
} catch (ParseException e) {
throw new IllegalStateException(e);
}
}

@Before
public void prepare() throws IOException {
String filePath = "xources/hello/HelloJava.xoo";
String md5sum = DigestUtils.md5Hex(FileUtils.readFileToString(new File(
Resources.getResource("mediumtest/xoo/sample/" + filePath).getPath())));

tester = BatchMediumTester.builder()
.bootstrapProperties(ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES))
.registerPlugin("xoo", new XooPlugin())
.addDefaultQProfile("xoo", "Sonar Way")
.addRules(new XooRulesDefinition())
.addRule("manual:MyManualIssue", "manual", "MyManualIssue", "My manual issue")
.addRule("manual:MyManualIssueDup", "manual", "MyManualIssue", "My manual issue")
.addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", null, "xoo")
.addActiveRule("xoo", "OneIssueOnDirPerFile", null, "OneIssueOnDirPerFile", "MAJOR", null, "xoo")
.addActiveRule("xoo", "OneIssuePerModule", null, "OneIssuePerModule", "MAJOR", null, "xoo")
.addActiveRule("manual", "MyManualIssue", null, "My manual issue", "MAJOR", null, null)
// this will cause the file to have status==SAME
.addFileData("sample", filePath, new FileData(md5sum, null))
.setPreviousAnalysisDate(new Date())
// Existing issue that is copied
.mockServerIssue(ServerIssue.newBuilder().setKey("xyz")
.setModuleKey("sample")
.setMsg("One issue per Line copied")
.setPath("xources/hello/HelloJava.xoo")
.setRuleRepository("xoo")
.setRuleKey("OneIssuePerLine")
.setLine(1)
.setSeverity(Severity.MAJOR)
.setCreationDate(date("14/03/2004"))
.setChecksum(DigestUtils.md5Hex("packagehello;"))
.setStatus("OPEN")
.build())
// Existing issue on project that is still detected
.mockServerIssue(ServerIssue.newBuilder().setKey("resolved-on-project")
.setModuleKey("sample")
.setRuleRepository("xoo")
.setRuleKey("OneIssuePerModule")
.setSeverity(Severity.CRITICAL)
.setCreationDate(date("14/03/2004"))
.setStatus("OPEN")
.build())
// Manual issue
.mockServerIssue(ServerIssue.newBuilder().setKey("manual")
.setModuleKey("sample")
.setPath("xources/hello/HelloJava.xoo")
.setRuleRepository("manual")
.setRuleKey("MyManualIssue")
.setLine(1)
.setSeverity(Severity.MAJOR)
.setCreationDate(date("14/03/2004"))
.setChecksum(DigestUtils.md5Hex("packagehello;"))
.setStatus("OPEN")
.build())
.build();
tester.start();
}

@After
public void stop() {
tester.stop();
}

private File copyProject(String path) throws Exception {
File projectDir = temp.newFolder();
File originalProjectDir = new File(IssueModeAndReportsMediumTest.class.getResource(path).toURI());
FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar")));
return projectDir;
}

@Test
public void testIssueTrackingChangedFiles() throws Exception {
File projectDir = copyProject("/mediumtest/xoo/sample");

TaskResult result = tester
.newScanTask(new File(projectDir, "sonar-project.properties"))
.property("sonar.scanChangedFilesOnly", "true")
.start();

int newIssues = 0;
int openIssues = 0;
int resolvedIssue = 0;
for (Issue issue : result.trackedIssues()) {
System.out
.println(issue.message() + " " + issue.key() + " " + issue.ruleKey() + " " + issue.isNew() + " " + issue.resolution() + " " + issue.componentKey() + " " + issue.line());
if (issue.isNew()) {
newIssues++;
} else if (issue.resolution() != null) {
resolvedIssue++;
} else {
openIssues++;
}
}
/*
* We have:
* 6 new issues per line (open) in helloscala.xoo
* 2 new issues per file in helloscala.xoo / ClassOneTest.xoo
* 1 server issue (open, not new) copied from server in HelloJava.xoo (this file is unchanged)
* 1 manual issue (open, not new) in HelloJava.xoo
* 1 existing issue on the project (open, not new)
*/
System.out.println("new: " + newIssues + " open: " + openIssues + " resolved " + resolvedIssue);
assertThat(newIssues).isEqualTo(8);
assertThat(openIssues).isEqualTo(3);
assertThat(resolvedIssue).isEqualTo(0);

// should only have server issues (HelloJava.xoo should not have been analyzed)
assertThat(result.trackedIssues()).haveExactly(2, new Condition<Issue>() {
@Override
public boolean matches(Issue value) {
return value.componentKey().endsWith("HelloJava.xoo");
}
});
}

}

+ 19
- 11
sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/ComponentIndexerTest.java View File

@@ -19,8 +19,13 @@
*/
package org.sonar.batch.scan.filesystem;

import org.sonar.api.batch.fs.InputFile.Status;

import org.sonar.batch.analysis.DefaultAnalysisMode;

import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Rule;
@@ -40,7 +45,6 @@ import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.Resource;
import org.sonar.batch.index.BatchComponent;
import org.sonar.batch.index.BatchComponentCache;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
@@ -57,6 +61,7 @@ public class ComponentIndexerTest {
private AbstractLanguage cobolLanguage;
private Project project;
private ModuleFileSystemInitializer initializer;
private DefaultAnalysisMode mode;

@Before
public void prepare() throws IOException {
@@ -65,6 +70,7 @@ public class ComponentIndexerTest {
sonarIndex = mock(SonarIndex.class);
project = new Project("myProject");
initializer = mock(ModuleFileSystemInitializer.class);
mode = mock(DefaultAnalysisMode.class);
when(initializer.baseDir()).thenReturn(baseDir);
when(initializer.workingDir()).thenReturn(temp.newFolder());
cobolLanguage = new AbstractLanguage("cobol") {
@@ -79,10 +85,11 @@ public class ComponentIndexerTest {
public void should_index_java_files() throws IOException {
Languages languages = new Languages(Java.INSTANCE);
ComponentIndexer indexer = createIndexer(languages);
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(project, null, mock(FileIndexer.class), initializer, indexer);
fs.add(newInputFile("src/main/java/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false));
fs.add(newInputFile("src/main/java2/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false));
fs.add(newInputFile("src/test/java/foo/bar/FooTest.java", "", "foo/bar/FooTest.java", "java", true));
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(project, null, mock(FileIndexer.class), initializer, indexer, mode);
fs.add(newInputFile("src/main/java/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false, Status.ADDED));
fs.add(newInputFile("src/main/java2/foo/bar/Foo.java", "", "foo/bar/Foo.java", "java", false, Status.ADDED));
// should index even if filter is applied
fs.add(newInputFile("src/test/java/foo/bar/FooTest.java", "", "foo/bar/FooTest.java", "java", true, Status.SAME));

fs.index();

@@ -110,10 +117,10 @@ public class ComponentIndexerTest {
public void should_index_cobol_files() throws IOException {
Languages languages = new Languages(cobolLanguage);
ComponentIndexer indexer = createIndexer(languages);
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(project, null, mock(FileIndexer.class), initializer, indexer);
fs.add(newInputFile("src/foo/bar/Foo.cbl", "", "foo/bar/Foo.cbl", "cobol", false));
fs.add(newInputFile("src2/foo/bar/Foo.cbl", "", "foo/bar/Foo.cbl", "cobol", false));
fs.add(newInputFile("src/test/foo/bar/FooTest.cbl", "", "foo/bar/FooTest.cbl", "cobol", true));
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(project, null, mock(FileIndexer.class), initializer, indexer, mode);
fs.add(newInputFile("src/foo/bar/Foo.cbl", "", "foo/bar/Foo.cbl", "cobol", false, Status.ADDED));
fs.add(newInputFile("src2/foo/bar/Foo.cbl", "", "foo/bar/Foo.cbl", "cobol", false, Status.ADDED));
fs.add(newInputFile("src/test/foo/bar/FooTest.cbl", "", "foo/bar/FooTest.cbl", "cobol", true, Status.ADDED));

fs.index();

@@ -122,12 +129,13 @@ public class ComponentIndexerTest {
verify(sonarIndex).index(org.sonar.api.resources.File.create("/src/test/foo/bar/FooTest.cbl", cobolLanguage, true));
}

private DefaultInputFile newInputFile(String path, String content, String sourceRelativePath, String languageKey, boolean unitTest) throws IOException {
private DefaultInputFile newInputFile(String path, String content, String sourceRelativePath, String languageKey, boolean unitTest, InputFile.Status status) throws IOException {
File file = new File(baseDir, path);
FileUtils.write(file, content);
return new DefaultInputFile("foo", path)
.setLanguage(languageKey)
.setType(unitTest ? InputFile.Type.TEST : InputFile.Type.MAIN);
.setType(unitTest ? InputFile.Type.TEST : InputFile.Type.MAIN)
.setStatus(status);
}

}

+ 51
- 14
sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/DefaultModuleFileSystemTest.java View File

@@ -19,6 +19,10 @@
*/
package org.sonar.batch.scan.filesystem;

import org.sonar.api.batch.fs.InputFile.Status;

import org.junit.Before;
import org.sonar.batch.analysis.DefaultAnalysisMode;
import com.google.common.collect.Lists;
import org.junit.Rule;
import org.junit.Test;
@@ -50,22 +54,33 @@ public class DefaultModuleFileSystemTest {
@Rule
public ExpectedException thrown = ExpectedException.none();

Settings settings = new Settings();
FileIndexer fileIndexer = mock(FileIndexer.class);
ModuleFileSystemInitializer initializer = mock(ModuleFileSystemInitializer.class, Mockito.RETURNS_DEEP_STUBS);
ComponentIndexer componentIndexer = mock(ComponentIndexer.class);
ModuleInputFileCache moduleInputFileCache = mock(ModuleInputFileCache.class);
private Settings settings;
private FileIndexer fileIndexer;
private ModuleFileSystemInitializer initializer;
private ComponentIndexer componentIndexer;
private ModuleInputFileCache moduleInputFileCache;
private DefaultAnalysisMode mode;

@Before
public void setUp() {
settings = new Settings();
fileIndexer = mock(FileIndexer.class);
initializer = mock(ModuleFileSystemInitializer.class, Mockito.RETURNS_DEEP_STUBS);
componentIndexer = mock(ComponentIndexer.class);
moduleInputFileCache = mock(ModuleInputFileCache.class);
mode = mock(DefaultAnalysisMode.class);
}

@Test
public void test_equals_and_hashCode() throws Exception {
DefaultModuleFileSystem foo1 = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
DefaultModuleFileSystem foo2 = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);
DefaultModuleFileSystem bar = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("bar"), settings, fileIndexer, initializer, componentIndexer);
new Project("bar"), settings, fileIndexer, initializer, componentIndexer, mode);
DefaultModuleFileSystem branch = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("bar", "branch", "My project"), settings, fileIndexer, initializer, componentIndexer);
new Project("bar", "branch", "My project"), settings, fileIndexer, initializer, componentIndexer, mode);

assertThat(foo1.moduleKey()).isEqualTo("foo");
assertThat(branch.moduleKey()).isEqualTo("bar:branch");
@@ -80,7 +95,7 @@ public class DefaultModuleFileSystemTest {
@Test
public void default_source_encoding() {
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);

assertThat(fs.sourceCharset()).isEqualTo(Charset.defaultCharset());
assertThat(fs.isDefaultJvmEncoding()).isTrue();
@@ -90,7 +105,7 @@ public class DefaultModuleFileSystemTest {
public void source_encoding_is_set() {
settings.setProperty(CoreProperties.ENCODING_PROPERTY, "Cp1124");
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);

assertThat(fs.encoding()).isEqualTo(Charset.forName("Cp1124"));
assertThat(fs.sourceCharset()).isEqualTo(Charset.forName("Cp1124"));
@@ -99,6 +114,28 @@ public class DefaultModuleFileSystemTest {
assertThat(fs.isDefaultJvmEncoding()).isFalse();
}

@Test
public void default_predicate_scan_only_changed() throws IOException {
when(mode.onlyAnalyzeChanged()).thenReturn(true);

DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);

File baseDir = temp.newFile();
InputFile mainInput = new DefaultInputFile("foo", "Main.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.MAIN);
InputFile testInput = new DefaultInputFile("foo", "Test.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.TEST);
InputFile mainSameInput = new DefaultInputFile("foo", "MainSame.java").setModuleBaseDir(baseDir.toPath())
.setType(InputFile.Type.TEST).setStatus(Status.SAME);
when(moduleInputFileCache.inputFiles()).thenReturn(Lists.newArrayList(mainInput, testInput, mainSameInput));

fs.index();
Iterable<InputFile> inputFiles = fs.inputFiles(fs.predicates().all());
assertThat(inputFiles).containsOnly(mainInput, testInput);

Iterable<InputFile> allInputFiles = fs.inputFiles();
assertThat(allInputFiles).containsOnly(mainInput, mainSameInput, testInput);
}

@Test
public void test_dirs() throws IOException {
File basedir = temp.newFolder("base");
@@ -120,7 +157,7 @@ public class DefaultModuleFileSystemTest {
when(initializer.tests()).thenReturn(Arrays.asList(javaTest, additionalTest));

DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);

assertThat(fs.baseDir().getCanonicalPath()).isEqualTo(basedir.getCanonicalPath());
assertThat(fs.workDir().getCanonicalPath()).isEqualTo(workingDir.getCanonicalPath());
@@ -133,7 +170,7 @@ public class DefaultModuleFileSystemTest {
@Test
public void should_search_input_files() throws Exception {
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);

File baseDir = temp.newFile();
InputFile mainInput = new DefaultInputFile("foo", "Main.java").setModuleBaseDir(baseDir.toPath()).setType(InputFile.Type.MAIN);
@@ -151,7 +188,7 @@ public class DefaultModuleFileSystemTest {
@Test
public void should_index() {
DefaultModuleFileSystem fs = new DefaultModuleFileSystem(moduleInputFileCache,
new Project("foo"), settings, fileIndexer, initializer, componentIndexer);
new Project("foo"), settings, fileIndexer, initializer, componentIndexer, mode);

verifyZeroInteractions(fileIndexer);


+ 22
- 3
sonar-plugin-api/src/main/java/org/sonar/api/batch/fs/internal/DefaultFileSystem.java View File

@@ -54,7 +54,8 @@ public class DefaultFileSystem implements FileSystem {
private final Path baseDir;
private Path workDir;
private Charset encoding;
private final FilePredicates predicates;
protected final FilePredicates predicates;
private FilePredicate defaultPredicate;

/**
* Only for testing
@@ -104,7 +105,12 @@ public class DefaultFileSystem implements FileSystem {
this.workDir = d.getAbsoluteFile().toPath().normalize();
return this;
}

public DefaultFileSystem setDefaultPredicate(@Nullable FilePredicate predicate) {
this.defaultPredicate = predicate;
return this;
}
@Override
public File workDir() {
return workDir.toFile();
@@ -135,11 +141,24 @@ public class DefaultFileSystem implements FileSystem {
throw new IllegalArgumentException(sb.toString());

}
/**
* Bypass default predicate to get all files/dirs indexed.
* Default predicate is used when some files/dirs should not be processed by sensors.
*/
public Iterable<InputFile> inputFiles() {
doPreloadFiles();
return OptimizedFilePredicateAdapter.create(predicates.all()).get(cache);
}

@Override
public Iterable<InputFile> inputFiles(FilePredicate predicate) {
doPreloadFiles();
return OptimizedFilePredicateAdapter.create(predicate).get(cache);
FilePredicate combinedPredicate = predicate;
if(defaultPredicate != null) {
combinedPredicate = predicates().and(defaultPredicate, predicate);
}
return OptimizedFilePredicateAdapter.create(combinedPredicate).get(cache);
}

@Override

Loading…
Cancel
Save