diff options
author | Lukasz Jarocki <lukasz.jarocki@sonarsource.com> | 2023-03-17 10:05:42 +0100 |
---|---|---|
committer | Lukasz Jarocki <lukasz.jarocki@sonarsource.com> | 2023-03-17 10:45:58 +0100 |
commit | c9c41f52a93f25286a9d516caa58fcca089ffbc4 (patch) | |
tree | 828aee6829652026930cc18879dbc216f7e1b8d3 /sonar-scanner-engine/src/it | |
parent | 83e185826c57a1d287d0f622fc50f80017fad944 (diff) | |
download | sonarqube-c9c41f52a93f25286a9d516caa58fcca089ffbc4.tar.gz sonarqube-c9c41f52a93f25286a9d516caa58fcca089ffbc4.zip |
SONAR-18679 moved unit test to integration test in sonar-scanner-engine
Diffstat (limited to 'sonar-scanner-engine/src/it')
35 files changed, 6250 insertions, 0 deletions
diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java new file mode 100644 index 00000000000..a7a181a6f43 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java @@ -0,0 +1,582 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.Priority; +import org.apache.commons.io.FileUtils; +import org.junit.rules.ExternalResource; +import org.sonar.api.Plugin; +import org.sonar.api.SonarEdition; +import org.sonar.api.SonarProduct; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.rule.LoadedActiveRule; +import org.sonar.api.impl.server.RulesDefinitionContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.server.rule.RulesDefinition.Repository; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.Version; +import org.sonar.batch.bootstrapper.Batch; +import org.sonar.batch.bootstrapper.EnvironmentInformation; +import org.sonar.batch.bootstrapper.LogOutput; +import org.sonar.scanner.bootstrap.GlobalAnalysisMode; +import org.sonar.scanner.cache.AnalysisCacheLoader; +import org.sonar.scanner.protocol.internal.SensorCacheData; +import org.sonar.scanner.report.CeTaskReportDataHolder; +import org.sonar.scanner.repository.FileData; +import org.sonar.scanner.repository.MetricsRepository; +import org.sonar.scanner.repository.MetricsRepositoryLoader; +import org.sonar.scanner.repository.NewCodePeriodLoader; +import org.sonar.scanner.repository.ProjectRepositories; +import org.sonar.scanner.repository.ProjectRepositoriesLoader; +import org.sonar.scanner.repository.QualityProfileLoader; +import org.sonar.scanner.repository.SingleProjectRepository; +import org.sonar.scanner.repository.settings.GlobalSettingsLoader; +import org.sonar.scanner.repository.settings.ProjectSettingsLoader; +import org.sonar.scanner.rule.ActiveRulesLoader; +import org.sonar.scanner.rule.RulesLoader; +import org.sonar.scanner.scan.ScanProperties; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.BranchConfigurationLoader; +import org.sonar.scanner.scan.branch.BranchType; +import org.sonar.scanner.scan.branch.ProjectBranches; +import org.sonarqube.ws.NewCodePeriods; +import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile; +import org.sonarqube.ws.Rules.ListResponse.Rule; + +import static java.util.Collections.emptySet; + +/** + * Main utility class for writing scanner medium tests. + */ +public class ScannerMediumTester extends ExternalResource { + + private static Path userHome = null; + private final Map<String, String> globalProperties = new HashMap<>(); + private final FakeMetricsRepositoryLoader globalRefProvider = new FakeMetricsRepositoryLoader(); + private final FakeBranchConfigurationLoader branchConfigurationLoader = new FakeBranchConfigurationLoader(); + private final FakeBranchConfiguration branchConfiguration = new FakeBranchConfiguration(); + private final FakeProjectRepositoriesLoader projectRefProvider = new FakeProjectRepositoriesLoader(); + private final FakePluginInstaller pluginInstaller = new FakePluginInstaller(); + private final FakeGlobalSettingsLoader globalSettingsLoader = new FakeGlobalSettingsLoader(); + private final FakeProjectSettingsLoader projectSettingsLoader = new FakeProjectSettingsLoader(); + private final FakeNewCodePeriodLoader newCodePeriodLoader = new FakeNewCodePeriodLoader(); + private final FakeAnalysisCacheLoader analysisCacheLoader = new FakeAnalysisCacheLoader(); + private final FakeRulesLoader rulesLoader = new FakeRulesLoader(); + private final FakeQualityProfileLoader qualityProfiles = new FakeQualityProfileLoader(); + private final FakeActiveRulesLoader activeRules = new FakeActiveRulesLoader(); + private final FakeSonarRuntime sonarRuntime = new FakeSonarRuntime(); + private final CeTaskReportDataHolder reportMetadataHolder = new CeTaskReportDataHolderExt(); + private LogOutput logOutput = null; + + private static void createWorkingDirs() throws IOException { + destroyWorkingDirs(); + + userHome = java.nio.file.Files.createTempDirectory("mediumtest-userHome"); + } + + private static void destroyWorkingDirs() throws IOException { + if (userHome != null) { + FileUtils.deleteDirectory(userHome.toFile()); + userHome = null; + } + } + + public ScannerMediumTester setLogOutput(LogOutput logOutput) { + this.logOutput = logOutput; + return this; + } + + public ScannerMediumTester registerPlugin(String pluginKey, File location) { + return registerPlugin(pluginKey, location, 1L); + } + + public ScannerMediumTester registerPlugin(String pluginKey, File location, long lastUpdatedAt) { + pluginInstaller.add(pluginKey, location, lastUpdatedAt); + return this; + } + + public ScannerMediumTester registerPlugin(String pluginKey, Plugin instance) { + return registerPlugin(pluginKey, instance, 1L); + } + + public ScannerMediumTester registerPlugin(String pluginKey, Plugin instance, long lastUpdatedAt) { + pluginInstaller.add(pluginKey, instance, lastUpdatedAt); + return this; + } + + public ScannerMediumTester registerCoreMetrics() { + for (Metric<?> m : CoreMetrics.getMetrics()) { + registerMetric(m); + } + return this; + } + + public ScannerMediumTester registerMetric(Metric<?> metric) { + globalRefProvider.add(metric); + return this; + } + + public ScannerMediumTester addQProfile(String language, String name) { + qualityProfiles.add(language, name); + return this; + } + + public ScannerMediumTester addRule(Rule rule) { + rulesLoader.addRule(rule); + return this; + } + + public ScannerMediumTester addRule(String key, String repoKey, String internalKey, String name) { + Rule.Builder builder = Rule.newBuilder(); + builder.setKey(key); + builder.setRepository(repoKey); + if (internalKey != null) { + builder.setInternalKey(internalKey); + } + builder.setName(name); + + rulesLoader.addRule(builder.build()); + return this; + } + + public ScannerMediumTester addRules(RulesDefinition rulesDefinition) { + RulesDefinition.Context context = new RulesDefinitionContext(); + rulesDefinition.define(context); + List<Repository> repositories = context.repositories(); + for (Repository repo : repositories) { + for (RulesDefinition.Rule rule : repo.rules()) { + this.addRule(rule.key(), rule.repository().key(), rule.internalKey(), rule.name()); + } + } + return this; + } + + public ScannerMediumTester addDefaultQProfile(String language, String name) { + addQProfile(language, name); + return this; + } + + public ScannerMediumTester bootstrapProperties(Map<String, String> props) { + globalProperties.putAll(props); + return this; + } + + public ScannerMediumTester activateRule(LoadedActiveRule activeRule) { + activeRules.addActiveRule(activeRule); + return this; + } + + public ScannerMediumTester addActiveRule(String repositoryKey, String ruleKey, @Nullable String templateRuleKey, String name, @Nullable String severity, + @Nullable String internalKey, @Nullable String language) { + LoadedActiveRule r = new LoadedActiveRule(); + + r.setInternalKey(internalKey); + r.setRuleKey(RuleKey.of(repositoryKey, ruleKey)); + r.setName(name); + r.setTemplateRuleKey(templateRuleKey); + r.setLanguage(language); + r.setSeverity(severity); + r.setDeprecatedKeys(emptySet()); + + activeRules.addActiveRule(r); + return this; + } + + public ScannerMediumTester addFileData(String path, FileData fileData) { + projectRefProvider.addFileData(path, fileData); + return this; + } + + public ScannerMediumTester addGlobalServerSettings(String key, String value) { + globalSettingsLoader.getGlobalSettings().put(key, value); + return this; + } + + public ScannerMediumTester addProjectServerSettings(String key, String value) { + projectSettingsLoader.getProjectSettings().put(key, value); + return this; + } + + public ScannerMediumTester setNewCodePeriod(NewCodePeriods.NewCodePeriodType type, String value) { + newCodePeriodLoader.set(NewCodePeriods.ShowWSResponse.newBuilder().setType(type).setValue(value).build()); + return this; + } + + @Override + protected void before() { + try { + createWorkingDirs(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + registerCoreMetrics(); + globalProperties.put(GlobalAnalysisMode.MEDIUM_TEST_ENABLED, "true"); + globalProperties.put(ScanProperties.KEEP_REPORT_PROP_KEY, "true"); + globalProperties.put("sonar.userHome", userHome.toString()); + } + + @Override + protected void after() { + try { + destroyWorkingDirs(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public AnalysisBuilder newAnalysis() { + return new AnalysisBuilder(this); + } + + public AnalysisBuilder newAnalysis(File sonarProps) { + Properties prop = new Properties(); + try (Reader reader = new InputStreamReader(new FileInputStream(sonarProps), StandardCharsets.UTF_8)) { + prop.load(reader); + } catch (Exception e) { + throw new IllegalStateException("Unable to read configuration file", e); + } + AnalysisBuilder builder = new AnalysisBuilder(this); + builder.property("sonar.projectBaseDir", sonarProps.getParentFile().getAbsolutePath()); + for (Map.Entry<Object, Object> entry : prop.entrySet()) { + builder.property(entry.getKey().toString(), entry.getValue().toString()); + } + return builder; + } + + public static class AnalysisBuilder { + private final Map<String, String> taskProperties = new HashMap<>(); + private final ScannerMediumTester tester; + + public AnalysisBuilder(ScannerMediumTester tester) { + this.tester = tester; + } + + public AnalysisResult execute() { + AnalysisResult result = new AnalysisResult(); + Map<String, String> props = new HashMap<>(); + props.putAll(tester.globalProperties); + props.putAll(taskProperties); + + Batch.builder() + .setGlobalProperties(props) + .setEnableLoggingConfiguration(true) + .addComponents(new EnvironmentInformation("mediumTest", "1.0"), + tester.pluginInstaller, + tester.globalRefProvider, + tester.qualityProfiles, + tester.rulesLoader, + tester.branchConfigurationLoader, + tester.projectRefProvider, + tester.activeRules, + tester.globalSettingsLoader, + tester.projectSettingsLoader, + tester.newCodePeriodLoader, + tester.analysisCacheLoader, + tester.sonarRuntime, + tester.reportMetadataHolder, + result) + .setLogOutput(tester.logOutput) + .build().execute(); + + return result; + } + + public AnalysisBuilder properties(Map<String, String> props) { + taskProperties.putAll(props); + return this; + } + + public AnalysisBuilder property(String key, String value) { + taskProperties.put(key, value); + return this; + } + + } + + @Priority(1) + private static class FakeRulesLoader implements RulesLoader { + private List<Rule> rules = new LinkedList<>(); + + public FakeRulesLoader addRule(Rule rule) { + rules.add(rule); + return this; + } + + @Override + public List<Rule> load() { + return rules; + } + } + + @Priority(1) + private static class FakeActiveRulesLoader implements ActiveRulesLoader { + private List<LoadedActiveRule> activeRules = new LinkedList<>(); + + public void addActiveRule(LoadedActiveRule activeRule) { + this.activeRules.add(activeRule); + } + + @Override + public List<LoadedActiveRule> load(String qualityProfileKey) { + return activeRules; + } + } + + @Priority(1) + private static class FakeMetricsRepositoryLoader implements MetricsRepositoryLoader { + + private int metricId = 1; + + private List<Metric> metrics = new ArrayList<>(); + + @Override + public MetricsRepository load() { + return new MetricsRepository(metrics); + } + + public FakeMetricsRepositoryLoader add(Metric<?> metric) { + metric.setUuid("metric" + metricId++); + metrics.add(metric); + metricId++; + return this; + } + + } + + @Priority(1) + private static class FakeProjectRepositoriesLoader implements ProjectRepositoriesLoader { + private Map<String, FileData> fileDataMap = new HashMap<>(); + + @Override + public ProjectRepositories load(String projectKey, @Nullable String branchBase) { + return new SingleProjectRepository(fileDataMap); + } + + public FakeProjectRepositoriesLoader addFileData(String path, FileData fileData) { + fileDataMap.put(path, fileData); + return this; + } + + } + + @Priority(1) + private static class FakeBranchConfiguration implements BranchConfiguration { + + private BranchType branchType = BranchType.BRANCH; + private String branchName = null; + private String branchTarget = null; + private String referenceBranchName = null; + + @Override + public BranchType branchType() { + return branchType; + } + + @CheckForNull + @Override + public String branchName() { + return branchName; + } + + @CheckForNull + @Override + public String targetBranchName() { + return branchTarget; + } + + @CheckForNull + @Override + public String referenceBranchName() { + return referenceBranchName; + } + + @Override + public String pullRequestKey() { + return "1'"; + } + } + + @Priority(1) + private static class FakeSonarRuntime implements SonarRuntime { + + private SonarEdition edition; + + FakeSonarRuntime() { + this.edition = SonarEdition.COMMUNITY; + } + + @Override + public Version getApiVersion() { + return Version.create(7, 8); + } + + @Override + public SonarProduct getProduct() { + return SonarProduct.SONARQUBE; + } + + @Override + public SonarQubeSide getSonarQubeSide() { + return SonarQubeSide.SCANNER; + } + + @Override + public SonarEdition getEdition() { + return edition; + } + + public void setEdition(SonarEdition edition) { + this.edition = edition; + } + } + + public ScannerMediumTester setBranchType(BranchType branchType) { + branchConfiguration.branchType = branchType; + return this; + } + + public ScannerMediumTester setBranchName(String branchName) { + this.branchConfiguration.branchName = branchName; + return this; + } + + public ScannerMediumTester setBranchTarget(String branchTarget) { + this.branchConfiguration.branchTarget = branchTarget; + return this; + } + + public ScannerMediumTester setReferenceBranchName(String referenceBranchNam) { + this.branchConfiguration.referenceBranchName = referenceBranchNam; + return this; + } + + public ScannerMediumTester setEdition(SonarEdition edition) { + this.sonarRuntime.setEdition(edition); + return this; + } + + @Priority(1) + private class FakeBranchConfigurationLoader implements BranchConfigurationLoader { + @Override + public BranchConfiguration load(Map<String, String> projectSettings, ProjectBranches branches) { + return branchConfiguration; + } + } + + @Priority(1) + private static class FakeQualityProfileLoader implements QualityProfileLoader { + + private List<QualityProfile> qualityProfiles = new LinkedList<>(); + + public void add(String language, String name) { + qualityProfiles.add(QualityProfile.newBuilder() + .setLanguage(language) + .setKey(name) + .setName(name) + .setRulesUpdatedAt(DateUtils.formatDateTime(new Date(1234567891212L))) + .build()); + } + + @Override + public List<QualityProfile> load(String projectKey) { + return qualityProfiles; + } + } + + @Priority(1) + private static class FakeAnalysisCacheLoader implements AnalysisCacheLoader { + @Override + public Optional<SensorCacheData> load() { + return Optional.empty(); + } + } + + @Priority(1) + private static class FakeGlobalSettingsLoader implements GlobalSettingsLoader { + + private Map<String, String> globalSettings = new HashMap<>(); + + public Map<String, String> getGlobalSettings() { + return globalSettings; + } + + @Override + public Map<String, String> loadGlobalSettings() { + return Collections.unmodifiableMap(globalSettings); + } + } + + @Priority(1) + private static class FakeNewCodePeriodLoader implements NewCodePeriodLoader { + private NewCodePeriods.ShowWSResponse response; + + @Override + public NewCodePeriods.ShowWSResponse load(String projectKey, String branchName) { + return response; + } + + public void set(NewCodePeriods.ShowWSResponse response) { + this.response = response; + } + } + + @Priority(1) + private static class FakeProjectSettingsLoader implements ProjectSettingsLoader { + + private Map<String, String> projectSettings = new HashMap<>(); + + public Map<String, String> getProjectSettings() { + return projectSettings; + } + + @Override + public Map<String, String> loadProjectSettings() { + return Collections.unmodifiableMap(projectSettings); + } + } + + @Priority(1) + private static class CeTaskReportDataHolderExt extends CeTaskReportDataHolder { + + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/branch/BranchMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/branch/BranchMediumIT.java new file mode 100644 index 00000000000..02f68d9e74b --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/branch/BranchMediumIT.java @@ -0,0 +1,163 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.branch; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.FileMetadata; +import org.sonar.api.notifications.AnalysisWarnings; +import org.sonar.api.utils.log.LogTester; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.scanner.repository.FileData; +import org.sonar.scanner.scan.branch.BranchType; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; +import org.sonarqube.ws.NewCodePeriods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class BranchMediumIT { + + private static final String PROJECT_KEY = "sample"; + private static final String FILE_PATH = "HelloJava.xoo"; + private static final String FILE_CONTENT = "xoooo"; + public static final String ONE_ISSUE_PER_LINE_IS_RESTRICTED_TO_CHANGED_FILES_ONLY = "Sensor One Issue Per Line is restricted to changed files only"; + private File baseDir; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo"); + + @Before + public void prepare() throws IOException { + baseDir = temp.newFolder(); + Path filepath = baseDir.toPath().resolve(FILE_PATH); + Files.write(filepath, FILE_CONTENT.getBytes()); + + Path xooUtCoverageFile = baseDir.toPath().resolve(FILE_PATH + ".coverage"); + FileUtils.write(xooUtCoverageFile.toFile(), "1:2:2:1", StandardCharsets.UTF_8); + + String md5sum = new FileMetadata(mock(AnalysisWarnings.class)) + .readMetadata(Files.newInputStream(filepath), StandardCharsets.UTF_8, FILE_PATH) + .hash(); + tester.addFileData(FILE_PATH, new FileData(md5sum, "1.1")); + tester.setNewCodePeriod(NewCodePeriods.NewCodePeriodType.PREVIOUS_VERSION, ""); + } + + @Test + public void should_not_skip_report_for_unchanged_files_in_pr() { + // sanity check, normally report gets generated + AnalysisResult result = getResult(tester); + final DefaultInputFile file = (DefaultInputFile) result.inputFile(FILE_PATH); + assertThat(getResult(tester).getReportComponent(file)).isNotNull(); + int fileId = file.scannerId(); + assertThat(result.getReportReader().readChangesets(fileId)).isNotNull(); + assertThat(result.getReportReader().hasCoverage(fileId)).isTrue(); + assertThat(result.getReportReader().readFileSource(fileId)).isNotNull(); + + // file is not skipped for pull requests (need coverage, duplications coming soon) + AnalysisResult result2 = getResult(tester.setBranchType(BranchType.PULL_REQUEST)); + final DefaultInputFile fileInPr = (DefaultInputFile) result2.inputFile(FILE_PATH); + assertThat(result2.getReportComponent(fileInPr)).isNotNull(); + fileId = fileInPr.scannerId(); + assertThat(result2.getReportReader().readChangesets(fileId)).isNull(); + assertThat(result2.getReportReader().hasCoverage(fileId)).isTrue(); + assertThat(result2.getReportReader().readFileSource(fileId)).isNull(); + } + + @Test + public void shouldSkipSensorForUnchangedFilesOnPr() { + AnalysisResult result = getResult(tester + .setBranchName("myBranch") + .setBranchTarget("main") + .setBranchType(BranchType.PULL_REQUEST)); + final DefaultInputFile file = (DefaultInputFile) result.inputFile(FILE_PATH); + + List<ScannerReport.Issue> issues = result.issuesFor(file); + assertThat(issues).isEmpty(); + + assertThat(logTester.logs()).contains(ONE_ISSUE_PER_LINE_IS_RESTRICTED_TO_CHANGED_FILES_ONLY); + } + + @Test + public void shouldNotSkipSensorForUnchangedFilesOnBranch() throws Exception { + AnalysisResult result = getResult(tester + .setBranchName("myBranch") + .setBranchTarget("main") + .setBranchType(BranchType.BRANCH)); + final DefaultInputFile file = (DefaultInputFile) result.inputFile(FILE_PATH); + + List<ScannerReport.Issue> issues = result.issuesFor(file); + assertThat(issues).isNotEmpty(); + + assertThat(logTester.logs()).doesNotContain(ONE_ISSUE_PER_LINE_IS_RESTRICTED_TO_CHANGED_FILES_ONLY); + } + + @Test + public void verify_metadata() { + String branchName = "feature"; + String branchTarget = "branch-1.x"; + + AnalysisResult result = getResult(tester + .setBranchName(branchName) + .setBranchTarget(branchTarget) + .setReferenceBranchName(branchTarget) + .setBranchType(BranchType.BRANCH)); + + ScannerReport.Metadata metadata = result.getReportReader().readMetadata(); + assertThat(metadata.getBranchName()).isEqualTo(branchName); + assertThat(metadata.getBranchType()).isEqualTo(ScannerReport.Metadata.BranchType.BRANCH); + assertThat(metadata.getReferenceBranchName()).isEqualTo(branchTarget); + } + + private AnalysisResult getResult(ScannerMediumTester tester) { + return tester + .newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", PROJECT_KEY) + .put("sonar.scm.provider", "xoo") + .build()) + .execute(); + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/branch/DeprecatedBranchMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/branch/DeprecatedBranchMediumIT.java new file mode 100644 index 00000000000..a6453f2f12e --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/branch/DeprecatedBranchMediumIT.java @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.branch; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.utils.MessageException; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeprecatedBranchMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addRules(new XooRulesDefinition()) + // active a rule just to be sure that xoo files are published + .addActiveRule("xoo", "xoo:OneIssuePerFile", null, "One Issue Per File", null, null, null) + .addDefaultQProfile("xoo", "Sonar Way"); + + private File baseDir; + + private Map<String, String> commonProps; + + @Before + public void prepare() { + baseDir = temp.getRoot(); + + commonProps = ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build(); + } + + @Test + public void scanProjectWithBranch() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\ncontent"); + + assertThatThrownBy(() -> tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .putAll(commonProps) + .put("sonar.branch", "branch") + .build()) + .execute()) + .isInstanceOf(MessageException.class) + .hasMessage("The 'sonar.branch' parameter is no longer supported. You should stop using it. " + + "Branch analysis is available in Developer Edition and above. See https://www.sonarsource.com/plans-and-pricing/developer/ for more information."); + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/coverage/CoverageMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/coverage/CoverageMediumIT.java new file mode 100644 index 00000000000..08faf0f0f5e --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/coverage/CoverageMediumIT.java @@ -0,0 +1,368 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.coverage; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.tuple; + +public class CoverageMediumIT { + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Test + public void singleReport() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File xooUtCoverageFile = new File(srcDir, "sample.xoo.coverage"); + FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFile, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.coverageFor(file, 2).getHits()).isTrue(); + assertThat(result.coverageFor(file, 2).getConditions()).isEqualTo(2); + assertThat(result.coverageFor(file, 2).getCoveredConditions()).isOne(); + } + + @Test + public void twoReports() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + File xooUtCoverageFile = new File(srcDir, "sample.xoo.coverage"); + FileUtils.write(xooUtCoverageFile, "2:2:2:2\n4:0", StandardCharsets.UTF_8); + File xooItCoverageFile = new File(srcDir, "sample.xoo.itcoverage"); + FileUtils.write(xooItCoverageFile, "2:0:2:1\n3:1\n5:0", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.coverageFor(file, 2).getHits()).isTrue(); + assertThat(result.coverageFor(file, 2).getConditions()).isEqualTo(2); + assertThat(result.coverageFor(file, 2).getCoveredConditions()).isEqualTo(2); + assertThat(result.coverageFor(file, 3).getHits()).isTrue(); + } + + @Test + public void exclusionsForSimpleProject() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File xooUtCoverageFile = new File(srcDir, "sample.xoo.coverage"); + FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFile, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.coverage.exclusions", "**/sample.xoo") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.coverageFor(file, 2)).isNull(); + } + + @Test + public void warn_user_for_outdated_inherited_scanner_side_exclusions_for_multi_module_project() throws IOException { + + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + File xooUtCoverageFileA = new File(srcDirA, "sampleA.xoo.coverage"); + FileUtils.write(xooFileA, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFileA, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + File xooFileB = new File(srcDirB, "sampleB.xoo"); + File xooUtCoverageFileB = new File(srcDirB, "sampleB.xoo.coverage"); + FileUtils.write(xooFileB, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFileB, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.coverage.exclusions", "src/sampleA.xoo") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sampleA.xoo"); + assertThat(result.coverageFor(fileA, 2)).isNull(); + + InputFile fileB = result.inputFile("moduleB/src/sampleB.xoo"); + assertThat(result.coverageFor(fileB, 2)).isNotNull(); + + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Specifying module-relative paths at project level in the property 'sonar.coverage.exclusions' is deprecated. " + + "To continue matching files like 'moduleA/src/sampleA.xoo', update this property so that patterns refer to project-relative paths."); + } + + @Test + public void module_level_exclusions_override_parent_for_multi_module_project() throws IOException { + + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + File xooUtCoverageFileA = new File(srcDirA, "sampleA.xoo.coverage"); + FileUtils.write(xooFileA, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFileA, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + File xooFileB = new File(srcDirB, "sampleB.xoo"); + File xooUtCoverageFileB = new File(srcDirB, "sampleB.xoo.coverage"); + FileUtils.write(xooFileB, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFileB, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.coverage.exclusions", "**/*.xoo") + .put("moduleA.sonar.coverage.exclusions", "**/*.nothing") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sampleA.xoo"); + assertThat(result.coverageFor(fileA, 2)).isNotNull(); + + InputFile fileB = result.inputFile("moduleB/src/sampleB.xoo"); + assertThat(result.coverageFor(fileB, 2)).isNull(); + } + + @Test + public void warn_user_for_outdated_server_side_exclusions_for_multi_module_project() throws IOException { + + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sample.xoo"); + File xooUtCoverageFileA = new File(srcDirA, "sample.xoo.coverage"); + FileUtils.write(xooFileA, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFileA, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + File xooFileB = new File(srcDirB, "sample.xoo"); + File xooUtCoverageFileB = new File(srcDirB, "sample.xoo.coverage"); + FileUtils.write(xooFileB, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(xooUtCoverageFileB, "2:2:2:1\n3:1", StandardCharsets.UTF_8); + + tester.addProjectServerSettings("sonar.coverage.exclusions", "src/sample.xoo"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sample.xoo"); + assertThat(result.coverageFor(fileA, 2)).isNull(); + + InputFile fileB = result.inputFile("moduleB/src/sample.xoo"); + assertThat(result.coverageFor(fileB, 2)).isNull(); + + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Specifying module-relative paths at project level in the property 'sonar.coverage.exclusions' is deprecated. " + + "To continue matching files like 'moduleA/src/sample.xoo', update this property so that patterns refer to project-relative paths."); + } + + @Test + public void fallbackOnExecutableLines() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File measuresFile = new File(srcDir, "sample.xoo.measures"); + FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(measuresFile, "executable_lines_data:2=1;3=1;4=0", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.coverageFor(file, 1)).isNull(); + + assertThat(result.coverageFor(file, 2).getHits()).isFalse(); + assertThat(result.coverageFor(file, 2).getConditions()).isZero(); + assertThat(result.coverageFor(file, 2).getCoveredConditions()).isZero(); + + assertThat(result.coverageFor(file, 3).getHits()).isFalse(); + assertThat(result.coverageFor(file, 4)).isNull(); + + assertThat(result.allMeasures().get(file.key())) + .extracting(ScannerReport.Measure::getMetricKey, m -> m.getStringValue().getValue()) + .contains(tuple("executable_lines_data", "2=1;3=1;4=0")); + } + + // SONAR-11641 + @Test + public void dontFallbackOnExecutableLinesIfNoCoverageSaved() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File measuresFile = new File(srcDir, "sample.xoo.measures"); + File coverageFile = new File(srcDir, "sample.xoo.coverage"); + FileUtils.write(xooFile, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(measuresFile, "# The code analyzer disagree with the coverage tool and consider some lines to be executable\nexecutable_lines_data:2=1;3=1;4=0", + StandardCharsets.UTF_8); + FileUtils.write(coverageFile, "# No lines to cover in this file according to the coverage tool", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.coverageFor(file, 1)).isNull(); + assertThat(result.coverageFor(file, 2)).isNull(); + assertThat(result.coverageFor(file, 3)).isNull(); + assertThat(result.coverageFor(file, 4)).isNull(); + } + + // SONAR-9557 + @Test + public void exclusionsAndForceToZeroOnModules() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "module1/src"); + srcDir.mkdir(); + + File xooFile1 = new File(srcDir, "sample1.xoo"); + File measuresFile1 = new File(srcDir, "sample1.xoo.measures"); + FileUtils.write(xooFile1, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(measuresFile1, "executable_lines_data:2=1;3=1;4=0", StandardCharsets.UTF_8); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + File measuresFile2 = new File(srcDir, "sample2.xoo.measures"); + FileUtils.write(xooFile2, "function foo() {\n if (a && b) {\nalert('hello');\n}\n}", StandardCharsets.UTF_8); + FileUtils.write(measuresFile2, "executable_lines_data:2=1;3=1;4=0", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "module1") + .put("sonar.sources", "src") + .put("sonar.coverage.exclusions", "**/sample2.xoo") + .build()) + .execute(); + + InputFile file1 = result.inputFile("module1/src/sample1.xoo"); + assertThat(result.coverageFor(file1, 1)).isNull(); + + assertThat(result.coverageFor(file1, 2).getHits()).isFalse(); + assertThat(result.coverageFor(file1, 2).getConditions()).isZero(); + assertThat(result.coverageFor(file1, 2).getCoveredConditions()).isZero(); + + assertThat(result.coverageFor(file1, 3).getHits()).isFalse(); + assertThat(result.coverageFor(file1, 4)).isNull(); + + InputFile file2 = result.inputFile("module1/src/sample2.xoo"); + assertThat(result.coverageFor(file2, 1)).isNull(); + assertThat(result.coverageFor(file2, 2)).isNull(); + assertThat(result.coverageFor(file2, 3)).isNull(); + assertThat(result.coverageFor(file2, 4)).isNull(); + + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/coverage/GenericCoverageMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/coverage/GenericCoverageMediumIT.java new file mode 100644 index 00000000000..1f16e7241d7 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/coverage/GenericCoverageMediumIT.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.coverage; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GenericCoverageMediumIT { + private final List<String> logs = new ArrayList<>(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Test + public void singleReport() { + + File projectDir = new File("test-resources/mediumtest/xoo/sample-generic-coverage"); + + AnalysisResult result = tester + .setLogOutput((msg, level) -> logs.add(msg)) + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.coverageReportPaths", "coverage.xml") + .execute(); + + InputFile noConditions = result.inputFile("xources/hello/NoConditions.xoo"); + assertThat(result.coverageFor(noConditions, 6).getHits()).isTrue(); + assertThat(result.coverageFor(noConditions, 6).getConditions()).isZero(); + assertThat(result.coverageFor(noConditions, 6).getCoveredConditions()).isZero(); + + assertThat(result.coverageFor(noConditions, 7).getHits()).isFalse(); + + InputFile withConditions = result.inputFile("xources/hello/WithConditions.xoo"); + assertThat(result.coverageFor(withConditions, 3).getHits()).isTrue(); + assertThat(result.coverageFor(withConditions, 3).getConditions()).isEqualTo(2); + assertThat(result.coverageFor(withConditions, 3).getCoveredConditions()).isOne(); + + assertThat(logs).noneMatch(l -> l.contains("Please use 'sonar.coverageReportPaths'")); + + } + + @Test + public void twoReports() { + + File projectDir = new File("test-resources/mediumtest/xoo/sample-generic-coverage"); + + AnalysisResult result = tester + .setLogOutput((msg, level) -> logs.add(msg)) + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.coverageReportPaths", "coverage.xml,coverage2.xml") + .execute(); + + InputFile noConditions = result.inputFile("xources/hello/NoConditions.xoo"); + assertThat(result.coverageFor(noConditions, 6).getHits()).isTrue(); + assertThat(result.coverageFor(noConditions, 6).getConditions()).isZero(); + assertThat(result.coverageFor(noConditions, 6).getCoveredConditions()).isZero(); + + assertThat(result.coverageFor(noConditions, 7).getHits()).isTrue(); + + InputFile withConditions = result.inputFile("xources/hello/WithConditions.xoo"); + assertThat(result.coverageFor(withConditions, 3).getHits()).isTrue(); + assertThat(result.coverageFor(withConditions, 3).getConditions()).isEqualTo(2); + assertThat(result.coverageFor(withConditions, 3).getCoveredConditions()).isEqualTo(2); + + assertThat(logs).noneMatch(l -> l.contains("Please use 'sonar.coverageReportPaths'")); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/cpd/CpdMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/cpd/CpdMediumIT.java new file mode 100644 index 00000000000..0975bb74a74 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/cpd/CpdMediumIT.java @@ -0,0 +1,473 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.cpd; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CpdMediumIT { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + private File baseDir; + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRules(new XooRulesDefinition()) + // active a rule just to be sure that xoo files are published + .addActiveRule("xoo", "xoo:OneIssuePerFile", null, "One Issue Per File", null, null, null); + + private ImmutableMap.Builder<String, String> builder; + + @Before + public void prepare() { + baseDir = temp.getRoot(); + + builder = ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project"); + } + + @Test + public void testCrossModuleDuplications() throws IOException { + builder.put("sonar.modules", "module1,module2") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.verbose", "true"); + + // module 1 + builder.put("module1.sonar.projectKey", "module1"); + builder.put("module1.sonar.projectName", "Module 1"); + builder.put("module1.sonar.sources", "."); + + // module2 + builder.put("module2.sonar.projectKey", "module2"); + builder.put("module2.sonar.projectName", "Module 2"); + builder.put("module2.sonar.sources", "."); + + File module1Dir = new File(baseDir, "module1"); + File module2Dir = new File(baseDir, "module2"); + + module1Dir.mkdir(); + module2Dir.mkdir(); + + String duplicatedStuff = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + // create duplicated file in both modules + File xooFile1 = new File(module1Dir, "sample1.xoo"); + FileUtils.write(xooFile1, duplicatedStuff); + + File xooFile2 = new File(module2Dir, "sample2.xoo"); + FileUtils.write(xooFile2, duplicatedStuff); + + AnalysisResult result = tester.newAnalysis().properties(builder.build()).execute(); + + assertThat(result.inputFiles()).hasSize(2); + + InputFile inputFile1 = result.inputFile("module1/sample1.xoo"); + InputFile inputFile2 = result.inputFile("module2/sample2.xoo"); + + // One clone group on each file + List<ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).hasSize(1); + + ScannerReport.Duplication cloneGroupFile1 = duplicationGroupsFile1.get(0); + assertThat(cloneGroupFile1.getOriginPosition().getStartLine()).isOne(); + assertThat(cloneGroupFile1.getOriginPosition().getEndLine()).isEqualTo(17); + assertThat(cloneGroupFile1.getDuplicateList()).hasSize(1); + assertThat(cloneGroupFile1.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(inputFile2).getRef()); + + List<ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).hasSize(1); + + ScannerReport.Duplication cloneGroupFile2 = duplicationGroupsFile2.get(0); + assertThat(cloneGroupFile2.getOriginPosition().getStartLine()).isOne(); + assertThat(cloneGroupFile2.getOriginPosition().getEndLine()).isEqualTo(17); + assertThat(cloneGroupFile2.getDuplicateList()).hasSize(1); + assertThat(cloneGroupFile2.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(inputFile1).getRef()); + + assertThat(result.duplicationBlocksFor(inputFile1)).isEmpty(); + } + + @Test + public void testCrossFileDuplications() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String duplicatedStuff = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, duplicatedStuff, StandardCharsets.UTF_8); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + FileUtils.write(xooFile2, duplicatedStuff, StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.verbose", "true") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + + InputFile inputFile1 = result.inputFile("src/sample1.xoo"); + InputFile inputFile2 = result.inputFile("src/sample2.xoo"); + + // One clone group on each file + List<ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).hasSize(1); + + ScannerReport.Duplication cloneGroupFile1 = duplicationGroupsFile1.get(0); + assertThat(cloneGroupFile1.getOriginPosition().getStartLine()).isOne(); + assertThat(cloneGroupFile1.getOriginPosition().getEndLine()).isEqualTo(17); + assertThat(cloneGroupFile1.getDuplicateList()).hasSize(1); + assertThat(cloneGroupFile1.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(inputFile2).getRef()); + + List<ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).hasSize(1); + + ScannerReport.Duplication cloneGroupFile2 = duplicationGroupsFile2.get(0); + assertThat(cloneGroupFile2.getOriginPosition().getStartLine()).isOne(); + assertThat(cloneGroupFile2.getOriginPosition().getEndLine()).isEqualTo(17); + assertThat(cloneGroupFile2.getDuplicateList()).hasSize(1); + assertThat(cloneGroupFile2.getDuplicate(0).getOtherFileRef()).isEqualTo(result.getReportComponent(inputFile1).getRef()); + + assertThat(result.duplicationBlocksFor(inputFile1)).isEmpty(); + } + + @Test + public void testFilesWithoutBlocks() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String file1 = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + String file2 = "string\n"; + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, file1, StandardCharsets.UTF_8); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + FileUtils.write(xooFile2, file2, StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.verbose", "true") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + + assertThat(logTester.logs()).contains("Not enough content in 'src/sample2.xoo' to have CPD blocks, it will not be part of the duplication detection"); + assertThat(logTester.logs()).contains("CPD Executor 1 file had no CPD blocks"); + + } + + @Test + public void testExclusions() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String duplicatedStuff = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, duplicatedStuff); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + FileUtils.write(xooFile2, duplicatedStuff); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.cpd.exclusions", "src/sample1.xoo") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + + InputFile inputFile1 = result.inputFile("src/sample1.xoo"); + InputFile inputFile2 = result.inputFile("src/sample2.xoo"); + + List<ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).isEmpty(); + + List<ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).isEmpty(); + } + + @Test + public void cross_module_duplication() throws IOException { + + String duplicatedStuff = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + FileUtils.write(xooFileA, duplicatedStuff, StandardCharsets.UTF_8); + + File xooFileB = new File(srcDirB, "sampleB.xoo"); + FileUtils.write(xooFileB, duplicatedStuff, StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.cpd.xoo.minimumTokens", "10") + .build()) + .execute(); + + InputFile inputFile1 = result.inputFile("moduleA/src/sampleA.xoo"); + InputFile inputFile2 = result.inputFile("moduleB/src/sampleB.xoo"); + + List<ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).isNotEmpty(); + + List<ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).isNotEmpty(); + } + + @Test + public void warn_user_for_outdated_inherited_scanner_side_exclusions_for_multi_module_project() throws IOException { + + String duplicatedStuff = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + FileUtils.write(xooFileA, duplicatedStuff, StandardCharsets.UTF_8); + + File xooFileB = new File(srcDirB, "sampleB.xoo"); + FileUtils.write(xooFileB, duplicatedStuff, StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.cpd.exclusions", "src/sampleA.xoo") + .build()) + .execute(); + + InputFile inputFile1 = result.inputFile("moduleA/src/sampleA.xoo"); + InputFile inputFile2 = result.inputFile("moduleB/src/sampleB.xoo"); + + List<ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).isEmpty(); + + List<ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).isEmpty(); + + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Specifying module-relative paths at project level in the property 'sonar.cpd.exclusions' is deprecated. " + + "To continue matching files like 'moduleA/src/sampleA.xoo', update this property so that patterns refer to project-relative paths."); + } + + @Test + public void module_level_exclusions_override_parent_for_multi_module_project() throws IOException { + + String duplicatedStuff = "Sample xoo\ncontent\n" + + "foo\nbar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti\n" + + "bar\ntoto\ntiti\n" + + "foo\nbar\ntoto\ntiti"; + + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + FileUtils.write(xooFileA, duplicatedStuff, StandardCharsets.UTF_8); + + File xooFileB = new File(srcDirB, "sampleB.xoo"); + FileUtils.write(xooFileB, duplicatedStuff, StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.cpd.xoo.minimumTokens", "10") + .put("sonar.cpd.exclusions", "**/*") + .put("moduleA.sonar.cpd.exclusions", "**/*.nothing") + .put("moduleB.sonar.cpd.exclusions", "**/*.nothing") + .build()) + .execute(); + + InputFile inputFile1 = result.inputFile("moduleA/src/sampleA.xoo"); + InputFile inputFile2 = result.inputFile("moduleB/src/sampleB.xoo"); + + List<ScannerReport.Duplication> duplicationGroupsFile1 = result.duplicationsFor(inputFile1); + assertThat(duplicationGroupsFile1).isNotEmpty(); + + List<ScannerReport.Duplication> duplicationGroupsFile2 = result.duplicationsFor(inputFile2); + assertThat(duplicationGroupsFile2).isNotEmpty(); + } + + @Test + public void enableCrossProjectDuplication() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String duplicatedStuff = "Sample xoo\ncontent\nfoo\nbar\ntoto\ntiti\nfoo"; + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, duplicatedStuff); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "1") + .put("sonar.cpd.xoo.minimumLines", "5") + .put("sonar.verbose", "true") + .put("sonar.cpd.cross_project", "true") + .build()) + .execute(); + + InputFile inputFile1 = result.inputFile("src/sample1.xoo"); + + List<ScannerReport.CpdTextBlock> duplicationBlocks = result.duplicationBlocksFor(inputFile1); + assertThat(duplicationBlocks).hasSize(3); + assertThat(duplicationBlocks.get(0).getStartLine()).isOne(); + assertThat(duplicationBlocks.get(0).getEndLine()).isEqualTo(5); + assertThat(duplicationBlocks.get(0).getStartTokenIndex()).isOne(); + assertThat(duplicationBlocks.get(0).getEndTokenIndex()).isEqualTo(6); + assertThat(duplicationBlocks.get(0).getHash()).isNotEmpty(); + + assertThat(duplicationBlocks.get(1).getStartLine()).isEqualTo(2); + assertThat(duplicationBlocks.get(1).getEndLine()).isEqualTo(6); + assertThat(duplicationBlocks.get(1).getStartTokenIndex()).isEqualTo(3); + assertThat(duplicationBlocks.get(1).getEndTokenIndex()).isEqualTo(7); + assertThat(duplicationBlocks.get(0).getHash()).isNotEmpty(); + + assertThat(duplicationBlocks.get(2).getStartLine()).isEqualTo(3); + assertThat(duplicationBlocks.get(2).getEndLine()).isEqualTo(7); + assertThat(duplicationBlocks.get(2).getStartTokenIndex()).isEqualTo(4); + assertThat(duplicationBlocks.get(2).getEndTokenIndex()).isEqualTo(8); + assertThat(duplicationBlocks.get(0).getHash()).isNotEmpty(); + } + + @Test + public void testIntraFileDuplications() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + String content = "Sample xoo\ncontent\nfoo\nbar\nSample xoo\ncontent\n"; + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, content); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.cpd.xoo.minimumTokens", "2") + .put("sonar.cpd.xoo.minimumLines", "2") + .put("sonar.verbose", "true") + .build()) + .execute(); + + InputFile inputFile = result.inputFile("src/sample.xoo"); + // One clone group + List<ScannerReport.Duplication> duplicationGroups = result.duplicationsFor(inputFile); + assertThat(duplicationGroups).hasSize(1); + + ScannerReport.Duplication cloneGroup = duplicationGroups.get(0); + assertThat(cloneGroup.getOriginPosition().getStartLine()).isOne(); + assertThat(cloneGroup.getOriginPosition().getEndLine()).isEqualTo(2); + assertThat(cloneGroup.getDuplicateList()).hasSize(1); + assertThat(cloneGroup.getDuplicate(0).getRange().getStartLine()).isEqualTo(5); + assertThat(cloneGroup.getDuplicate(0).getRange().getEndLine()).isEqualTo(6); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java new file mode 100644 index 00000000000..9c1c55338b3 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/FileSystemMediumIT.java @@ -0,0 +1,1282 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.fs; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Random; +import java.util.stream.Collectors; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.CoreProperties; +import org.sonar.api.SonarEdition; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.PathUtils; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.mediumtest.ScannerMediumTester.AnalysisBuilder; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.global.DeprecatedGlobalSensor; +import org.sonar.xoo.global.GlobalProjectSensor; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +public class FileSystemMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .setEdition(SonarEdition.COMMUNITY) + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addDefaultQProfile("xoo2", "Sonar Way"); + + private File baseDir; + private ImmutableMap.Builder<String, String> builder; + + @Before + public void prepare() throws IOException { + baseDir = temp.newFolder().getCanonicalFile(); + + builder = ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project"); + } + + @Test + public void scan_project_without_name() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + int ref = result.getReportReader().readMetadata().getRootComponentRef(); + assertThat(result.getReportReader().readComponent(ref).getName()).isEmpty(); + assertThat(result.inputFiles()).hasSize(1); + + DefaultInputFile file = (DefaultInputFile) result.inputFile("src/sample.xoo"); + assertThat(file.type()).isEqualTo(InputFile.Type.MAIN); + assertThat(file.relativePath()).isEqualTo("src/sample.xoo"); + assertThat(file.language()).isEqualTo("xoo"); + + // file was published, since language matched xoo + assertThat(file.isPublished()).isTrue(); + assertThat(result.getReportComponent(file.scannerId())).isNotNull(); + } + + @Test + public void log_branch_name_and_type() { + builder.put("sonar.branch.name", "my-branch"); + File srcDir = new File(baseDir, "src"); + assertThat(srcDir.mkdir()).isTrue(); + + // Using sonar.branch.name when the branch plugin is not installed is an error. + assertThatThrownBy(() -> tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute()) + .isInstanceOf(MessageException.class); + } + + @Test + public void only_generate_metadata_if_needed() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDir, "sample.java", "Sample xoo\ncontent"); + + logTester.setLevel(LoggerLevel.DEBUG); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs()).contains("2 files indexed"); + assertThat(logTester.logs()).contains("'src/sample.xoo' generated metadata with charset 'UTF-8'"); + assertThat(String.join("\n", logTester.logs())).doesNotContain("'src/sample.java' generated metadata"); + } + + @Test + public void preload_file_metadata() throws IOException { + builder.put("sonar.preloadFileMetadata", "true"); + + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDir, "sample.java", "Sample xoo\ncontent"); + + logTester.setLevel(LoggerLevel.DEBUG); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs()).contains("2 files indexed"); + assertThat(logTester.logs()).contains("'src/sample.xoo' generated metadata with charset 'UTF-8'"); + assertThat(logTester.logs()).contains("'src/sample.java' generated metadata with charset 'UTF-8'"); + } + + @Test + public void dont_publish_files_without_detected_language() throws IOException { + Path mainDir = baseDir.toPath().resolve("src").resolve("main"); + Files.createDirectories(mainDir); + + Path testDir = baseDir.toPath().resolve("src").resolve("test"); + Files.createDirectories(testDir); + + writeFile(testDir.toFile(), "sample.java", "Sample xoo\ncontent"); + writeFile(mainDir.toFile(), "sample.xoo", "Sample xoo\ncontent"); + writeFile(mainDir.toFile(), "sample.java", "Sample xoo\ncontent"); + + logTester.setLevel(LoggerLevel.DEBUG); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src/main") + .put("sonar.tests", "src/test") + .build()) + .execute(); + + assertThat(logTester.logs()).containsAnyOf("'src/main/sample.java' indexed with no language", "'src\\main\\sample.java' indexed with no language"); + assertThat(logTester.logs()).contains("3 files indexed"); + assertThat(logTester.logs()).contains("'src/main/sample.xoo' generated metadata with charset 'UTF-8'"); + assertThat(logTester.logs()).doesNotContain("'src/main/sample.java' generated metadata", "'src\\main\\sample.java' generated metadata"); + assertThat(logTester.logs()).doesNotContain("'src/test/sample.java' generated metadata", "'src\\test\\sample.java' generated metadata"); + DefaultInputFile javaInputFile = (DefaultInputFile) result.inputFile("src/main/sample.java"); + + assertThatThrownBy(() -> result.getReportComponent(javaInputFile)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Unable to find report for component"); + } + + @Test + public void create_issue_on_any_file() throws IOException { + tester + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssuePerUnknownFile", null, "OneIssuePerUnknownFile", "MAJOR", null, "xoo"); + + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.unknown", "Sample xoo\ncontent"); + + logTester.setLevel(LoggerLevel.DEBUG); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs()).contains("1 file indexed"); + assertThat(logTester.logs()).contains("'src" + File.separator + "sample.unknown' indexed with no language"); + assertThat(logTester.logs()).contains("'src/sample.unknown' generated metadata with charset 'UTF-8'"); + DefaultInputFile inputFile = (DefaultInputFile) result.inputFile("src/sample.unknown"); + assertThat(result.getReportComponent(inputFile)).isNotNull(); + } + + @Test + public void lazyIssueExclusion() throws IOException { + tester + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssuePerFile", null, "OneIssuePerFile", "MAJOR", null, "xoo"); + + builder.put("sonar.issue.ignore.allfile", "1") + .put("sonar.issue.ignore.allfile.1.fileRegexp", "pattern"); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + + File unknownFile = new File(srcDir, "myfile.binary"); + byte[] b = new byte[512]; + new Random().nextBytes(b); + FileUtils.writeByteArrayToFile(unknownFile, b); + + logTester.setLevel(LoggerLevel.DEBUG); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs()).containsOnlyOnce("'src" + File.separator + "myfile.binary' indexed with no language"); + assertThat(logTester.logs()).doesNotContain("Evaluate issue exclusions for 'src/myfile.binary'"); + assertThat(logTester.logs()).containsOnlyOnce("Evaluate issue exclusions for 'src/sample.xoo'"); + } + + @Test + public void preloadIssueExclusions() throws IOException { + builder.put("sonar.issue.ignore.allfile", "1") + .put("sonar.issue.ignore.allfile.1.fileRegexp", "pattern") + .put("sonar.preloadFileMetadata", "true"); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\npattern"); + writeFile(srcDir, "myfile.binary", "some text"); + + logTester.setLevel(LoggerLevel.DEBUG); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs()).containsSequence("Evaluate issue exclusions for 'src/sample.xoo'", + " - Exclusion pattern 'pattern': all issues in this file will be ignored."); + } + + @Test + public void publishFilesWithIssues() throws IOException { + tester + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssueOnDirPerFile", null, "OneIssueOnDirPerFile", "MAJOR", null, "xoo"); + + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + DefaultInputFile file = (DefaultInputFile) result.inputFile("src/sample.xoo"); + + assertThat(file.isPublished()).isTrue(); + assertThat(result.getReportComponent(file)).isNotNull(); + } + + @Test + public void scanProjectWithSourceDir() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + assertThat(result.inputFile("src/sample.xoo").type()).isEqualTo(InputFile.Type.MAIN); + assertThat(result.inputFile("src/sample.xoo").relativePath()).isEqualTo("src/sample.xoo"); + } + + @Test + public void scanBigProject() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + int nbFiles = 100; + int ruleCount = 100000; + for (int nb = 1; nb <= nbFiles; nb++) { + File xooFile = new File(srcDir, "sample" + nb + ".xoo"); + FileUtils.write(xooFile, StringUtils.repeat(StringUtils.repeat("a", 100) + "\n", ruleCount / 1000)); + } + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(100); + } + + @Test + public void scanProjectWithTestDir() throws IOException { + File test = new File(baseDir, "test"); + test.mkdir(); + + writeFile(test, "sampleTest.xoo", "Sample test xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "") + .put("sonar.tests", "test") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + assertThat(result.inputFile("test/sampleTest.xoo").type()).isEqualTo(InputFile.Type.TEST); + } + + /** + * SONAR-5419 + */ + @Test + public void scanProjectWithMixedSourcesAndTests() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + writeFile(baseDir, "another.xoo", "Sample xoo 2\ncontent"); + + File testDir = new File(baseDir, "test"); + testDir.mkdir(); + + writeFile(baseDir, "sampleTest2.xoo", "Sample test xoo\ncontent"); + writeFile(testDir, "sampleTest.xoo", "Sample test xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src,another.xoo") + .put("sonar.tests", "test,sampleTest2.xoo") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(4); + } + + @Test + public void fileInclusionsExclusions() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + writeFile(baseDir, "another.xoo", "Sample xoo 2\ncontent"); + + File testDir = new File(baseDir, "test"); + testDir.mkdir(); + + writeFile(baseDir, "sampleTest2.xoo", "Sample test xoo\ncontent"); + writeFile(testDir, "sampleTest.xoo", "Sample test xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src,another.xoo") + .put("sonar.tests", "test,sampleTest2.xoo") + .put("sonar.inclusions", "src/**") + .put("sonar.exclusions", "**/another.*") + .put("sonar.test.inclusions", "**/sampleTest*.*") + .put("sonar.test.exclusions", "**/sampleTest2.xoo") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + } + + @Test + public void ignoreFilesWhenGreaterThanDefinedSize() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File fileGreaterThanLimit = writeFile(srcDir, "sample.xoo", 1024 * 1024 + 1); + writeFile(srcDir, "another.xoo", "Sample xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + // set limit to 1MB + .put("sonar.filesize.limit", "1") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + assertThat(logTester.logs()) + .contains(format("File '%s' is bigger than 1MB and as consequence is removed from the analysis scope.", fileGreaterThanLimit.getAbsolutePath())); + } + + @Test + public void analysisFailsSymbolicLinkWithoutTargetIsInTheFolder() throws IOException { + assumeFalse(SystemUtils.IS_OS_WINDOWS); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File target = writeFile(srcDir, "target.xoo", 1024 * 1024 + 1); + Path link = Paths.get(srcDir.getPath(), "target_link.xoo"); + Files.createSymbolicLink(link, target.toPath()); + Files.delete(target.toPath()); + + AnalysisBuilder analysis = tester.newAnalysis() + .properties(builder.build()); + + assertThatThrownBy(analysis::execute) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessageEndingWith(format("Unable to read file %s", link)); + } + + @Test + public void test_inclusions_on_multi_modules() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "tests"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "tests"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sampleTestA.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sampleTestB.xoo", "Sample xoo\ncontent"); + + final ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "") + .put("sonar.tests", "tests") + .put("sonar.modules", "moduleA,moduleB"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder.build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + + InputFile fileA = result.inputFile("moduleA/tests/sampleTestA.xoo"); + assertThat(fileA).isNotNull(); + + InputFile fileB = result.inputFile("moduleB/tests/sampleTestB.xoo"); + assertThat(fileB).isNotNull(); + + result = tester.newAnalysis() + .properties(builder + .put("sonar.test.inclusions", "moduleA/tests/**") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + + fileA = result.inputFile("moduleA/tests/sampleTestA.xoo"); + assertThat(fileA).isNotNull(); + + fileB = result.inputFile("moduleB/tests/sampleTestB.xoo"); + assertThat(fileB).isNull(); + } + + @Test + public void test_module_level_inclusions_override_parent_on_multi_modules() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sampleA.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sampleB.xoo", "Sample xoo\ncontent"); + + final ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.inclusions", "**/*.php"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder.build()) + .execute(); + + assertThat(result.inputFiles()).isEmpty(); + + result = tester.newAnalysis() + .properties(builder + .put("moduleA.sonar.inclusions", "**/*.xoo") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + + InputFile fileA = result.inputFile("moduleA/src/sampleA.xoo"); + assertThat(fileA).isNotNull(); + } + + @Test + public void warn_user_for_outdated_scanner_side_inherited_exclusions_for_multi_module_project() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.exclusions", "src/sample.xoo") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sample.xoo"); + assertThat(fileA).isNull(); + + InputFile fileB = result.inputFile("moduleB/src/sample.xoo"); + assertThat(fileB).isNull(); + + assertThat(logTester.logs(LoggerLevel.WARN)) + .contains("Specifying module-relative paths at project level in the property 'sonar.exclusions' is deprecated. " + + "To continue matching files like 'moduleA/src/sample.xoo', update this property so that patterns refer to project-relative paths."); + } + + @Test + public void support_global_server_side_exclusions_for_multi_module_project() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent"); + + tester.addGlobalServerSettings(CoreProperties.PROJECT_EXCLUSIONS_PROPERTY, "**/*.xoo"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sample.xoo"); + assertThat(fileA).isNull(); + + InputFile fileB = result.inputFile("moduleB/src/sample.xoo"); + assertThat(fileB).isNull(); + } + + @Test + public void support_global_server_side_global_exclusions_for_multi_module_project() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent"); + + tester.addGlobalServerSettings(CoreProperties.GLOBAL_EXCLUSIONS_PROPERTY, "**/*.xoo"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sample.xoo"); + assertThat(fileA).isNull(); + + InputFile fileB = result.inputFile("moduleB/src/sample.xoo"); + assertThat(fileB).isNull(); + } + + @Test + public void warn_user_for_outdated_server_side_inherited_exclusions_for_multi_module_project() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent"); + + tester.addProjectServerSettings("sonar.exclusions", "src/sample.xoo"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .build()) + .execute(); + + InputFile fileA = result.inputFile("moduleA/src/sample.xoo"); + assertThat(fileA).isNull(); + + InputFile fileB = result.inputFile("moduleB/src/sample.xoo"); + assertThat(fileB).isNull(); + + assertThat(logTester.logs(LoggerLevel.WARN)) + .contains("Specifying module-relative paths at project level in the property 'sonar.exclusions' is deprecated. " + + "To continue matching files like 'moduleA/src/sample.xoo', update this property so that patterns refer to project-relative paths."); + } + + @Test + public void failForDuplicateInputFile() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + + assertThatThrownBy(() -> tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src,src/sample.xoo") + .build()) + .execute()) + .isInstanceOf(MessageException.class) + .hasMessage("File src/sample.xoo can't be indexed twice. Please check that inclusion/exclusion patterns produce disjoint sets for main and test files"); + } + + // SONAR-9574 + @Test + public void failForDuplicateInputFileInDifferentModules() throws IOException { + File srcDir = new File(baseDir, "module1/src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + + assertThatThrownBy(() -> tester.newAnalysis() + .properties(builder + .put("sonar.sources", "module1/src") + .put("sonar.modules", "module1") + .put("module1.sonar.sources", "src") + .build()) + .execute()) + .isInstanceOf(MessageException.class) + .hasMessage("File module1/src/sample.xoo can't be indexed twice. Please check that inclusion/exclusion patterns produce disjoint sets for main and test files"); + } + + // SONAR-5330 + @Test + public void scanProjectWithSourceSymlink() { + assumeTrue(!System2.INSTANCE.isOsWindows()); + File projectDir = new File("test-resources/mediumtest/xoo/sample-with-symlink"); + AnalysisResult result = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.exclusions", "**/*.xoo.measures,**/*.xoo.scm") + .property("sonar.test.exclusions", "**/*.xoo.measures,**/*.xoo.scm") + .property("sonar.scm.exclusions.disabled", "true") + .execute(); + + assertThat(result.inputFiles()).hasSize(3); + // check that symlink was not resolved to target + assertThat(result.inputFiles()).extractingResultOf("path").toString().startsWith(projectDir.toString()); + } + + // SONAR-6719 + @Test + public void scanProjectWithWrongCase() { + // To please the quality gate, don't use assumeTrue, or the test will be reported as skipped + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + AnalysisBuilder analysis = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.sources", "XOURCES") + .property("sonar.tests", "TESTX") + .property("sonar.scm.exclusions.disabled", "true"); + + if (System2.INSTANCE.isOsWindows()) { // Windows is file path case-insensitive + AnalysisResult result = analysis.execute(); + + assertThat(result.inputFiles()).hasSize(8); + assertThat(result.inputFiles()).extractingResultOf("relativePath").containsOnly( + "testx/ClassOneTest.xoo.measures", + "xources/hello/helloscala.xoo.measures", + "xources/hello/HelloJava.xoo.measures", + "testx/ClassOneTest.xoo", + "xources/hello/HelloJava.xoo.scm", + "xources/hello/helloscala.xoo", + "testx/ClassOneTest.xoo.scm", + "xources/hello/HelloJava.xoo"); + } else if (System2.INSTANCE.isOsMac()) { + AnalysisResult result = analysis.execute(); + + assertThat(result.inputFiles()).hasSize(8); + assertThat(result.inputFiles()).extractingResultOf("relativePath").containsOnly( + "TESTX/ClassOneTest.xoo.measures", + "XOURCES/hello/helloscala.xoo.measures", + "XOURCES/hello/HelloJava.xoo.measures", + "TESTX/ClassOneTest.xoo", + "XOURCES/hello/HelloJava.xoo.scm", + "XOURCES/hello/helloscala.xoo", + "TESTX/ClassOneTest.xoo.scm", + "XOURCES/hello/HelloJava.xoo"); + } else { + assertThatThrownBy(() -> analysis.execute()) + .isInstanceOf(MessageException.class) + .hasMessageContaining("The folder 'TESTX' does not exist for 'sample'"); + } + } + + @Test + public void indexAnyFile() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDir, "sample.other", "Sample other\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + assertThat(result.inputFile("src/sample.other").type()).isEqualTo(InputFile.Type.MAIN); + assertThat(result.inputFile("src/sample.other").relativePath()).isEqualTo("src/sample.other"); + assertThat(result.inputFile("src/sample.other").language()).isNull(); + } + + @Test + public void scanMultiModuleProject() { + File projectDir = new File("test-resources/mediumtest/xoo/multi-modules-sample"); + AnalysisResult result = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .execute(); + + assertThat(result.inputFiles()).hasSize(4); + } + + @Test + public void deprecated_global_sensor_should_see_project_relative_paths() { + File projectDir = new File("test-resources/mediumtest/xoo/multi-modules-sample"); + AnalysisResult result = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property(DeprecatedGlobalSensor.ENABLE_PROP, "true") + .execute(); + + assertThat(result.inputFiles()).hasSize(4); + assertThat(logTester.logs(LoggerLevel.INFO)).contains( + "Deprecated Global Sensor: module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo", + "Deprecated Global Sensor: module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo", + "Deprecated Global Sensor: module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo", + "Deprecated Global Sensor: module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo"); + } + + @Test + public void global_sensor_should_see_project_relative_paths() { + File projectDir = new File("test-resources/mediumtest/xoo/multi-modules-sample"); + AnalysisResult result = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property(GlobalProjectSensor.ENABLE_PROP, "true") + .execute(); + + assertThat(result.inputFiles()).hasSize(4); + assertThat(logTester.logs(LoggerLevel.INFO)).contains( + "Global Sensor: module_a/module_a1/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo", + "Global Sensor: module_a/module_a2/src/main/xoo/com/sonar/it/samples/modules/a2/HelloA2.xoo", + "Global Sensor: module_b/module_b1/src/main/xoo/com/sonar/it/samples/modules/b1/HelloB1.xoo", + "Global Sensor: module_b/module_b2/src/main/xoo/com/sonar/it/samples/modules/b2/HelloB2.xoo"); + } + + @Test + public void scan_project_with_comma_in_source_path() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample,1.xoo", "Sample xoo\ncontent"); + writeFile(baseDir, "another,2.xoo", "Sample xoo 2\ncontent"); + + File testDir = new File(baseDir, "test"); + testDir.mkdir(); + + writeFile(testDir, "sampleTest,1.xoo", "Sample test xoo\ncontent"); + writeFile(baseDir, "sampleTest,2.xoo", "Sample test xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src,\"another,2.xoo\"") + .put("sonar.tests", "\"test\",\"sampleTest,2.xoo\"") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(4); + } + + @Test + public void language_without_publishAllFiles_should_not_auto_publish_files() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo3", "Sample xoo\ncontent"); + writeFile(srcDir, "sample2.xoo3", "Sample xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.inputFiles()) + .extracting(InputFile::filename, InputFile::language, f -> ((DefaultInputFile) f).isPublished()) + .containsOnly(tuple("sample.xoo3", "xoo3", false), tuple("sample2.xoo3", "xoo3", false)); + assertThat(result.getReportReader().readComponent(result.getReportReader().readMetadata().getRootComponentRef()).getChildRefCount()).isZero(); + } + + @Test + public void two_languages_with_same_extension() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDir, "sample.xoo2", "Sample xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(2); + + AnalysisBuilder analysisBuilder = tester.newAnalysis() + .properties(builder + .put("sonar.lang.patterns.xoo2", "**/*.xoo") + .build()); + + assertThatThrownBy(analysisBuilder::execute) + .isInstanceOf(MessageException.class) + .hasMessage( + "Language of file 'src" + File.separator + + "sample.xoo' can not be decided as the file matches patterns of both sonar.lang.patterns.xoo : **/*.xoo and sonar.lang.patterns.xoo2 : **/*.xoo"); + + // SONAR-9561 + result = tester.newAnalysis() + .properties(builder + .put("sonar.exclusions", "**/sample.xoo") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + } + + @Test + public void log_all_exclusions_properties_per_modules() throws IOException { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + writeFile(srcDirA, "sample.xoo", "Sample xoo\ncontent"); + writeFile(srcDirB, "sample.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.inclusions", "**/global.inclusions") + .put("sonar.test.inclusions", "**/global.test.inclusions") + .put("sonar.exclusions", "**/global.exclusions") + .put("sonar.test.exclusions", "**/global.test.exclusions") + .put("sonar.coverage.exclusions", "**/coverage.exclusions") + .put("sonar.cpd.exclusions", "**/cpd.exclusions") + .build()) + .execute(); + + assertThat(logTester.logs(LoggerLevel.INFO)) + .containsSequence("Project configuration:", + " Included sources: **/global.inclusions", + " Excluded sources: **/global.exclusions, **/global.test.inclusions", + " Included tests: **/global.test.inclusions", + " Excluded tests: **/global.test.exclusions", + " Excluded sources for coverage: **/coverage.exclusions", + " Excluded sources for duplication: **/cpd.exclusions", + "Indexing files of module 'moduleA'", + " Base dir: " + baseDirModuleA.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS), + " Source paths: src", + " Included sources: **/global.inclusions", + " Excluded sources: **/global.exclusions, **/global.test.inclusions", + " Included tests: **/global.test.inclusions", + " Excluded tests: **/global.test.exclusions", + " Excluded sources for coverage: **/coverage.exclusions", + " Excluded sources for duplication: **/cpd.exclusions", + "Indexing files of module 'moduleB'", + " Base dir: " + baseDirModuleB.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS), + " Source paths: src", + " Included sources: **/global.inclusions", + " Excluded sources: **/global.exclusions, **/global.test.inclusions", + " Included tests: **/global.test.inclusions", + " Excluded tests: **/global.test.exclusions", + " Excluded sources for coverage: **/coverage.exclusions", + " Excluded sources for duplication: **/cpd.exclusions", + "Indexing files of module 'com.foo.project'", + " Base dir: " + baseDir.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS), + " Included sources: **/global.inclusions", + " Excluded sources: **/global.exclusions, **/global.test.inclusions", + " Included tests: **/global.test.inclusions", + " Excluded tests: **/global.test.exclusions", + " Excluded sources for coverage: **/coverage.exclusions", + " Excluded sources for duplication: **/cpd.exclusions"); + } + + @Test + public void ignore_files_outside_project_basedir() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample1.xoo", "Sample xoo\ncontent"); + + File outsideBaseDir = temp.newFolder().getCanonicalFile(); + File xooFile2 = writeFile(outsideBaseDir, "another.xoo", "Sample xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src," + PathUtils.canonicalPath(xooFile2)) + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("File '" + xooFile2.getAbsolutePath() + "' is ignored. It is not located in project basedir '" + baseDir + "'."); + } + + @Test + public void dont_log_warn_about_files_out_of_basedir_if_they_arent_included() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + writeFile(srcDir, "sample1.xoo", "Sample xoo\ncontent"); + + File outsideBaseDir = temp.newFolder().getCanonicalFile(); + File xooFile2 = new File(outsideBaseDir, "another.xoo"); + FileUtils.write(xooFile2, "Sample xoo 2\ncontent", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src," + PathUtils.canonicalPath(xooFile2)) + .put("sonar.inclusions", "**/sample1.xoo") + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.WARN)).doesNotContain("File '" + xooFile2.getAbsolutePath() + "' is ignored. It is not located in project basedir '" + baseDir + "'."); + } + + @Test + public void ignore_files_outside_module_basedir() throws IOException { + File moduleA = new File(baseDir, "moduleA"); + moduleA.mkdir(); + + writeFile(moduleA, "src/sampleA.xoo", "Sample xoo\ncontent"); + File xooFile2 = writeFile(baseDir, "another.xoo", "Sample xoo 2\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.modules", "moduleA") + .put("moduleA.sonar.sources", "src," + PathUtils.canonicalPath(xooFile2)) + .build()) + .execute(); + + assertThat(result.inputFiles()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.WARN)) + .contains("File '" + xooFile2.getAbsolutePath() + "' is ignored. It is not located in module basedir '" + new File(baseDir, "moduleA") + "'."); + } + + @Test + public void exclusion_based_on_scm_info() { + File projectDir = new File("test-resources/mediumtest/xoo/sample-with-ignored-file"); + AnalysisResult result = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.exclusions", "**/*.xoo.ignore") + .property("sonar.test.exclusions", "**/*.xoo.ignore") + .execute(); + + assertThat(result.inputFile("xources/hello/ClassTwo.xoo")).isNull(); + assertThat(result.inputFile("testx/ClassTwoTest.xoo")).isNull(); + + assertThat(result.inputFile("xources/hello/ClassOne.xoo")).isNotNull(); + assertThat(result.inputFile("testx/ClassOneTest.xoo")).isNotNull(); + } + + @Test + public void no_exclusion_when_scm_exclusions_is_disabled() { + File projectDir = new File("test-resources/mediumtest/xoo/sample-with-ignored-file"); + AnalysisResult result = tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.scm.exclusions.disabled", "true") + .property("sonar.exclusions", "**/*.xoo.ignore") + .property("sonar.test.exclusions", "**/*.xoo.ignore") + .execute(); + + assertThat(result.inputFiles()).hasSize(4); + + assertThat(result.inputFile("xources/hello/ClassTwo.xoo")).isNotNull(); + assertThat(result.inputFile("testx/ClassTwoTest.xoo")).isNotNull(); + assertThat(result.inputFile("xources/hello/ClassOne.xoo")).isNotNull(); + assertThat(result.inputFile("testx/ClassOneTest.xoo")).isNotNull(); + } + + @Test + public void index_basedir_by_default() throws IOException { + writeFile(baseDir, "sample.xoo", "Sample xoo\ncontent"); + AnalysisResult result = tester.newAnalysis() + .properties(builder + .build()) + .execute(); + + assertThat(logTester.logs()).contains("1 file indexed"); + assertThat(result.inputFile("sample.xoo")).isNotNull(); + } + + @Test + public void givenExclusionEndingWithOneWildcardWhenAnalysedThenOnlyDirectChildrenFilesShouldBeExcluded() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + writeFile(srcDir, "src.xoo", "Sample xoo 2\ncontent"); + + // src/srcSubDir/srcSub.xoo + File srcSubDir = createDir(srcDir, "srcSubDir", true); + writeFile(srcSubDir, "srcSub.xoo", "Sample xoo\ncontent"); + + // src/srcSubDir/srcSubSubDir/subSubSrc.xoo + File srcSubSubDir = createDir(srcSubDir, "srcSubSubDir", true); + writeFile(srcSubSubDir, "subSubSrc.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.exclusions", "src/srcSubDir/*") + .build()) + .execute(); + + assertAnalysedFiles(result, "src/src.xoo", "src/srcSubDir/srcSubSubDir/subSubSrc.xoo"); + } + + @Test + public void givenPathsWithoutReadPermissionWhenAllChildrenAreExcludedThenScannerShouldSkipIt() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + writeFile(srcDir, "src.xoo", "Sample xoo 2\ncontent"); + + // src/srcSubDir/srcSub.xoo + File srcSubDir = createDir(srcDir, "srcSubDir", false); + writeFile(srcSubDir, "srcSub.xoo", "Sample xoo\ncontent"); + + // src/srcSubDir2/srcSub2.xoo + File srcSubDir2 = createDir(srcDir, "srcSubDir2", true); + boolean fileNotReadable = writeFile(srcSubDir2, "srcSub2.xoo", "Sample 2 xoo\ncontent").setReadable(false); + assumeTrue(fileNotReadable); + + // src/srcSubDir2/srcSubSubDir2/srcSubSub2.xoo + File srcSubSubDir2 = createDir(srcSubDir2, "srcSubSubDir2", false); + writeFile(srcSubSubDir2, "srcSubSub2.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.exclusions", "src/srcSubDir/**/*,src/srcSubDir2/**/*") + .build()) + .execute(); + + assertAnalysedFiles(result, "src/src.xoo"); + assertThat(logTester.logs()).contains("1 file ignored because of inclusion/exclusion patterns"); + } + + @Test + public void givenFileWithoutAccessWhenChildrenAreExcludedThenThenScanShouldFail() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + boolean fileNotReadable = writeFile(srcDir, "src.xoo", "Sample xoo\ncontent").setReadable(false); + assumeTrue(fileNotReadable); + + AnalysisBuilder result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.exclusions", "src/src.xoo/**/*") // incorrect pattern, but still the scan should fail if src.xoo is not accessible + .build()); + + assertThatThrownBy(result::execute) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessageStartingWith(format("java.lang.IllegalStateException: Unable to read file")); + } + + @Test + public void givenDirectoryWithoutReadPermissionWhenIncludedThenScanShouldFail() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + writeFile(srcDir, "src.xoo", "Sample xoo 2\ncontent"); + + // src/srcSubDir/srcSub.xoo + File srcSubDir = createDir(srcDir, "srcSubDir", false); + writeFile(srcSubDir, "srcSub.xoo", "Sample xoo\ncontent"); + + AnalysisBuilder result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.exclusions", "src/srcSubDir/*") // srcSubDir should not be excluded unless all children are excluded (src/srcSubDir/**/*) + .build()); + + assertThatThrownBy(result::execute) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessageEndingWith(format("Failed to index files")); + } + + @Test + public void givenDirectoryWhenAllChildrenAreExcludedThenSkippedFilesShouldBeReported() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + writeFile(srcDir, "src.xoo", "Sample xoo 2\ncontent"); + + // src/srcSubDir/srcSub.xoo + File srcSubDir = createDir(srcDir, "srcSubDir", true); + writeFile(srcSubDir, "srcSub.xoo", "Sample xoo\ncontent"); + + // src/srcSubDir2/srcSub2.xoo + File srcSubDir2 = createDir(srcDir, "srcSubDir2", true); + boolean fileNotReadable = writeFile(srcSubDir2, "srcSub2.xoo", "Sample 2 xoo\ncontent").setReadable(false); + assumeTrue(fileNotReadable); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.exclusions", "src/srcSubDir/**/*,src/srcSubDir2/*") + .build()) + .execute(); + + assertAnalysedFiles(result, "src/src.xoo"); + assertThat(logTester.logs()).contains("2 files ignored because of inclusion/exclusion patterns"); + } + + @Ignore("Fails until we can pattern match inclusions to directories, not only files.") + @Test + public void givenDirectoryWithoutReadPermissionWhenNotIncludedThenScanShouldSkipIt() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + writeFile(srcDir, "src.xoo", "Sample xoo 2\ncontent"); + + // src/srcSubDir/srcSub.xoo + File srcSubDir = createDir(srcDir, "srcSubDir", true); + writeFile(srcSubDir, "srcSub.xoo", "Sample xoo\ncontent"); + + // src/srcSubDir2/srcSub2.xoo + File srcSubDir2 = createDir(srcDir, "srcSubDir2", false); + writeFile(srcSubDir2, "srcSub2.xoo", "Sample xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.inclusions", "src/srcSubDir/**/*") + .build()) + .execute(); + + assertAnalysedFiles(result, "src/srcSubDir/srcSub.xoo"); + } + + @Test + public void givenDirectoryWithoutReadPermissionUnderSourcesWhenAnalysedThenShouldFail() throws IOException { + // src/src.xoo + File srcDir = createDir(baseDir, "src", true); + writeFile(srcDir, "src.xoo", "Sample xoo 2\ncontent"); + + // src/srcSubDir/srcSub.xoo + File srcSubDir = createDir(srcDir, "srcSubDir", false); + writeFile(srcSubDir, "srcSub.xoo", "Sample xoo\ncontent"); + + AnalysisBuilder result = tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()); + + assertThatThrownBy(result::execute) + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessageEndingWith(format("Failed to index files")); + } + + private static void assertAnalysedFiles(AnalysisResult result, String... files) { + assertThat(result.inputFiles().stream().map(InputFile::toString).collect(Collectors.toList())).contains(files); + } + + private File createDir(File parentDir, String name, boolean isReadable) { + File dir = new File(parentDir, name); + dir.mkdir(); + boolean fileSystemOperationSucceded = dir.setReadable(isReadable); + assumeTrue(fileSystemOperationSucceded); //On windows + java there is no reliable way to play with readable/not readable flag + return dir; + } + + private File writeFile(File parent, String name, String content) throws IOException { + File file = new File(parent, name); + FileUtils.write(file, content, StandardCharsets.UTF_8); + return file; + } + + private File writeFile(File parent, String name, long size) throws IOException { + File file = new File(parent, name); + RandomAccessFile raf = new RandomAccessFile(file, "rw"); + raf.setLength(size); + raf.close(); + return file; + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/NoLanguagesPluginsMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/NoLanguagesPluginsMediumIT.java new file mode 100644 index 00000000000..a32056a4cb7 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/NoLanguagesPluginsMediumIT.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.fs; + +import java.io.File; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.springframework.beans.factory.BeanCreationException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NoLanguagesPluginsMediumIT { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester(); + + @Test + public void testNoLanguagePluginsInstalled() throws Exception { + File projectDir = copyProject("test-resources/mediumtest/xoo/sample"); + + assertThatThrownBy(() -> tester + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .execute()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseMessage("No language plugins are installed."); + } + + private File copyProject(String path) throws Exception { + File projectDir = temp.newFolder(); + File originalProjectDir = new File(path); + FileUtils.copyDirectory(originalProjectDir, projectDir, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".sonar"))); + return projectDir; + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/ProjectBuilderMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/ProjectBuilderMediumIT.java new file mode 100644 index 00000000000..d7e75c25173 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/fs/ProjectBuilderMediumIT.java @@ -0,0 +1,130 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.fs; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.SonarEdition; +import org.sonar.api.batch.bootstrap.ProjectBuilder; +import org.sonar.api.utils.MessageException; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport.Issue; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +public class ProjectBuilderMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private ProjectBuilder projectBuilder = mock(ProjectBuilder.class); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .setEdition(SonarEdition.SONARCLOUD) + .registerPlugin("xoo", new XooPluginWithBuilder(projectBuilder)) + .addRules(new XooRulesDefinition()) + .addDefaultQProfile("xoo", "Sonar Way") + .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo"); + + private static class XooPluginWithBuilder extends XooPlugin { + private ProjectBuilder builder; + + XooPluginWithBuilder(ProjectBuilder builder) { + this.builder = builder; + } + + @Override + public void define(Context context) { + super.define(context); + context.addExtension(builder); + } + } + + @Test + public void testProjectReactorValidation() throws IOException { + File baseDir = prepareProject(); + + doThrow(new IllegalStateException("My error message")).when(projectBuilder).build(any(ProjectBuilder.Context.class)); + + assertThatThrownBy(() -> tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", ".") + .put("sonar.xoo.enableProjectBuilder", "true") + .build()) + .execute()) + .isInstanceOf(MessageException.class) + .hasMessageContaining("Failed to execute project builder") + .hasCauseInstanceOf(IllegalStateException.class); + } + + @Test + public void testProjectBuilder() throws IOException { + File baseDir = prepareProject(); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", ".") + .put("sonar.verbose", "true") + .put("sonar.xoo.enableProjectBuilder", "true") + .build()) + .execute(); + List<Issue> issues = result.issuesFor(result.inputFile("module1/src/sample.xoo")); + assertThat(issues).hasSize(10); + + assertThat(issues) + .extracting("msg", "textRange.startLine", "gap") + .contains(tuple("This issue is generated on each line", 1, 0.0)); + + } + + private File prepareProject() throws IOException { + File baseDir = temp.newFolder(); + File module1Dir = new File(baseDir, "module1"); + module1Dir.mkdir(); + + File srcDir = new File(module1Dir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"); + + return baseDir; + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/highlighting/HighlightingMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/highlighting/HighlightingMediumIT.java new file mode 100644 index 00000000000..811c883099f --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/highlighting/HighlightingMediumIT.java @@ -0,0 +1,132 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.highlighting; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.highlighting.TypeOfText; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.scanner.mediumtest.ScannerMediumTester.AnalysisBuilder; + +public class HighlightingMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Test + public void computeSyntaxHighlightingOnTempProject() throws IOException { + + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File xoohighlightingFile = new File(srcDir, "sample.xoo.highlighting"); + FileUtils.write(xooFile, "Sample xoo\ncontent plop"); + FileUtils.write(xoohighlightingFile, "1:0:2:0:s\n2:0:2:8:k"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.highlightingTypeFor(file, 1, 0)).containsExactly(TypeOfText.STRING); + assertThat(result.highlightingTypeFor(file, 1, 9)).containsExactly(TypeOfText.STRING); + assertThat(result.highlightingTypeFor(file, 2, 0)).containsExactly(TypeOfText.KEYWORD); + assertThat(result.highlightingTypeFor(file, 2, 8)).isEmpty(); + } + + @Test + public void saveTwice() throws IOException { + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\ncontent plop"); + + AnalysisBuilder analysisBuilder = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .put("sonar.it.savedatatwice", "true") + .build());; + + assertThatThrownBy(() -> analysisBuilder.execute()) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("Trying to save highlighting twice for the same file is not supported"); + } + + @Test + public void computeInvalidOffsets() throws IOException { + + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File xoohighlightingFile = new File(srcDir, "sample.xoo.highlighting"); + FileUtils.write(xooFile, "Sample xoo\ncontent plop"); + FileUtils.write(xoohighlightingFile, "1:0:1:10:s\n2:18:2:18:k"); + + AnalysisBuilder analysisBuilder = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()); + + assertThatThrownBy(() -> analysisBuilder.execute()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Error processing line 2") + .hasCauseInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/ChecksMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/ChecksMediumIT.java new file mode 100644 index 00000000000..30db61751f4 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/ChecksMediumIT.java @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.rule.LoadedActiveRule; +import org.sonar.api.rule.RuleKey; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport.Issue; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class ChecksMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addRules(new XooRulesDefinition()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRule("TemplateRule_1234", "xoo", "TemplateRule_1234", "A template rule") + .addRule("TemplateRule_1235", "xoo", "TemplateRule_1235", "Another template rule") + .activateRule(createActiveRuleWithParam("xoo", "TemplateRule_1234", "TemplateRule", "A template rule", "MAJOR", null, "xoo", "line", "1")) + .activateRule(createActiveRuleWithParam("xoo", "TemplateRule_1235", "TemplateRule", "Another template rule", "MAJOR", null, "xoo", "line", "2")); + + @Test + public void testCheckWithTemplate() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "foo\nbar"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()) + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo")); + // If the message is the rule name. it's set by the CE. See SONAR-11531 + assertThat(issues) + .extracting("msg", "textRange.startLine") + .containsOnly( + tuple("", 1), + tuple("", 2)); + + } + + private LoadedActiveRule createActiveRuleWithParam(String repositoryKey, String ruleKey, @Nullable String templateRuleKey, String name, + @Nullable String severity, @Nullable String internalKey, @Nullable String languag, String paramKey, String paramValue) { + LoadedActiveRule r = new LoadedActiveRule(); + + r.setInternalKey(internalKey); + r.setRuleKey(RuleKey.of(repositoryKey, ruleKey)); + r.setName(name); + r.setTemplateRuleKey(templateRuleKey); + r.setLanguage(languag); + r.setSeverity(severity); + r.setDeprecatedKeys(emptySet()); + + Map<String, String> params = new HashMap<>(); + params.put(paramKey, paramValue); + r.setParams(params); + return r; + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumIT.java new file mode 100644 index 00000000000..2f9d939f7d8 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumIT.java @@ -0,0 +1,184 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.utils.log.LogTester; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.Constants.Severity; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue; +import org.sonar.scanner.protocol.output.ScannerReport.Issue; +import org.sonar.scanner.protocol.output.ScannerReport.IssueType; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.OneExternalIssuePerLineSensor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class ExternalIssuesMediumIT { + @Rule + public LogTester logs = new LogTester(); + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()); + + @Test + public void testOneIssuePerLine() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property(OneExternalIssuePerLineSensor.ACTIVATE, "true") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).isEmpty(); + + List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(externalIssues).hasSize(8 /* lines */); + + ExternalIssue issue = externalIssues.get(0); + assertThat(issue.getTextRange().getStartLine()).isEqualTo(issue.getTextRange().getStartLine()); + + assertThat(result.adHocRules()).isEmpty(); + } + + @Test + public void testOneIssuePerLine_register_ad_hoc_rule() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property(OneExternalIssuePerLineSensor.ACTIVATE, "true") + .property(OneExternalIssuePerLineSensor.REGISTER_AD_HOC_RULE, "true") + .execute(); + + assertThat(result.adHocRules()).extracting( + ScannerReport.AdHocRule::getEngineId, + ScannerReport.AdHocRule::getRuleId, + ScannerReport.AdHocRule::getName, + ScannerReport.AdHocRule::getDescription, + ScannerReport.AdHocRule::getSeverity, + ScannerReport.AdHocRule::getType) + .containsExactlyInAnyOrder( + tuple( + OneExternalIssuePerLineSensor.ENGINE_ID, + OneExternalIssuePerLineSensor.RULE_ID, + "An ad hoc rule", + "blah blah", + Severity.BLOCKER, + IssueType.BUG)); + } + + @Test + public void testLoadIssuesFromJsonReport() throws URISyntaxException, IOException { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property("sonar.externalIssuesReportPaths", "externalIssues.json") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).isEmpty(); + + List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(externalIssues).hasSize(2); + + // precise issue location + ExternalIssue issue = externalIssues.get(0); + assertPreciseIssueLocation(issue); + + // location on a line + issue = externalIssues.get(1); + assertIssueLocationLine(issue); + + // One file-level issue in helloscala, with secondary location + List<ExternalIssue> externalIssues2 = result.externalIssuesFor(result.inputFile("xources/hello/helloscala.xoo")); + assertThat(externalIssues2).hasSize(1); + + issue = externalIssues2.iterator().next(); + assertSecondaryLocation(issue); + + // one issue is located in a non-existing file + assertThat(logs.logs()).contains("External issues ignored for 1 unknown files, including: invalidFile"); + + } + + private void assertSecondaryLocation(ExternalIssue issue) { + assertThat(issue.getFlowCount()).isEqualTo(2); + assertThat(issue.getMsg()).isEqualTo("fix the bug here"); + assertThat(issue.getEngineId()).isEqualTo("externalXoo"); + assertThat(issue.getRuleId()).isEqualTo("rule3"); + assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR); + assertThat(issue.getType()).isEqualTo(IssueType.BUG); + assertThat(issue.hasTextRange()).isFalse(); + assertThat(issue.getFlow(0).getLocationCount()).isOne(); + assertThat(issue.getFlow(0).getLocation(0).getTextRange().getStartLine()).isOne(); + assertThat(issue.getFlow(1).getLocationCount()).isOne(); + assertThat(issue.getFlow(1).getLocation(0).getTextRange().getStartLine()).isEqualTo(3); + } + + private void assertIssueLocationLine(ExternalIssue issue) { + assertThat(issue.getFlowCount()).isZero(); + assertThat(issue.getMsg()).isEqualTo("fix the bug here"); + assertThat(issue.getEngineId()).isEqualTo("externalXoo"); + assertThat(issue.getRuleId()).isEqualTo("rule2"); + assertThat(issue.getSeverity()).isEqualTo(Severity.CRITICAL); + assertThat(issue.getType()).isEqualTo(IssueType.BUG); + assertThat(issue.getEffort()).isZero(); + assertThat(issue.getTextRange().getStartLine()).isEqualTo(3); + assertThat(issue.getTextRange().getEndLine()).isEqualTo(3); + assertThat(issue.getTextRange().getStartOffset()).isZero(); + assertThat(issue.getTextRange().getEndOffset()).isEqualTo(24); + } + + private void assertPreciseIssueLocation(ExternalIssue issue) { + assertThat(issue.getFlowCount()).isZero(); + assertThat(issue.getMsg()).isEqualTo("fix the issue here"); + assertThat(issue.getEngineId()).isEqualTo("externalXoo"); + assertThat(issue.getRuleId()).isEqualTo("rule1"); + assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR); + assertThat(issue.getEffort()).isEqualTo(50L); + assertThat(issue.getType()).isEqualTo(IssueType.CODE_SMELL); + assertThat(issue.getTextRange().getStartLine()).isEqualTo(5); + assertThat(issue.getTextRange().getEndLine()).isEqualTo(5); + assertThat(issue.getTextRange().getStartOffset()).isEqualTo(3); + assertThat(issue.getTextRange().getEndOffset()).isEqualTo(41); + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesMediumIT.java new file mode 100644 index 00000000000..feaf921d141 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesMediumIT.java @@ -0,0 +1,452 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.rule.LoadedActiveRule; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue; +import org.sonar.scanner.protocol.output.ScannerReport.Issue; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.HasTagSensor; +import org.sonar.xoo.rule.OneExternalIssueOnProjectSensor; +import org.sonar.xoo.rule.OneExternalIssuePerLineSensor; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class IssuesMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssuePerLine", null, "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo"); + + @Test + public void testOneIssuePerLine() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).hasSize(8 /* lines */); + + List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(externalIssues).isEmpty(); + } + + @Test + public void testOneExternalIssuePerLine() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property(OneExternalIssuePerLineSensor.ACTIVATE, "true") + .execute(); + + List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(externalIssues).hasSize(8 /* lines */); + } + + @Test + public void testOneExternalIssueOnProject() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property(OneExternalIssueOnProjectSensor.ACTIVATE, "true") + .execute(); + + List<ExternalIssue> externalIssues = result.externalIssuesFor(result.project()); + assertThat(externalIssues).hasSize(1); + } + + @Test + public void findActiveRuleByInternalKey() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property("sonar.xoo.internalKey", "OneIssuePerLine.internal") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).hasSize(8 /* lines */ + 1 /* file */); + } + + @Test + public void testOverrideQProfileSeverity() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property("sonar.oneIssuePerLine.forceSeverity", "CRITICAL") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues.get(0).getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL); + } + + @Test + public void testIssueExclusionByRegexp() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property("sonar.issue.ignore.allfile", "1") + .property("sonar.issue.ignore.allfile.1.fileRegexp", "object") + .execute(); + + assertThat(result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"))).hasSize(8 /* lines */); + assertThat(result.issuesFor(result.inputFile("xources/hello/helloscala.xoo"))).isEmpty(); + } + + @Test + public void testIssueExclusionByBlock() throws Exception { + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "1\nSONAR-OFF 2\n3\n4\n5\nSONAR-ON 6\n7\n8\n9\n10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .property("sonar.issue.ignore.block", "1") + .property("sonar.issue.ignore.block.1.beginBlockRegexp", "SON.*-OFF") + .property("sonar.issue.ignore.block.1.endBlockRegexp", "SON.*-ON") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo")); + assertThat(issues).hasSize(5); + assertThat(issues) + .extracting("textRange.startLine") + .containsExactlyInAnyOrder(1, 7, 8, 9, 10); + } + + @Test + public void testIssueExclusionByIgnoreMultiCriteria() throws Exception { + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + activateTODORule(); + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, "1\n2\n3 TODO\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + File xooFile11 = new File(srcDir, "sample11.xoo"); + FileUtils.write(xooFile11, "1\n2\n3 TODO\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .property("sonar.issue.ignore.multicriteria", "1,2") + .property("sonar.issue.ignore.multicriteria.1.ruleKey", "xoo:HasTag") + .property("sonar.issue.ignore.multicriteria.1.resourceKey", "src/sample11.xoo") + .property("sonar.issue.ignore.multicriteria.2.ruleKey", "xoo:One*") + .property("sonar.issue.ignore.multicriteria.2.resourceKey", "src/sample?.xoo") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("src/sample1.xoo")); + assertThat(issues).hasSize(2); + + issues = result.issuesFor(result.inputFile("src/sample11.xoo")); + assertThat(issues).hasSize(10); + } + + @Test + public void warn_user_for_outdated_IssueExclusionByIgnoreMultiCriteria() throws Exception { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + FileUtils.write(xooFileA, "1\n2\n3\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + File xooFileB = new File(srcDirB, "sampleB.xoo"); + FileUtils.write(xooFileB, "1\n2\n3\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + + tester + .addProjectServerSettings("sonar.issue.ignore.multicriteria", "1") + .addProjectServerSettings("sonar.issue.ignore.multicriteria.1.ruleKey", "*") + .addProjectServerSettings("sonar.issue.ignore.multicriteria.1.resourceKey", "src/sampleA.xoo"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs(LoggerLevel.WARN)).contains( + "Specifying module-relative paths at project level in property 'sonar.issue.ignore.multicriteria' is deprecated. To continue matching files like 'moduleA/src/sampleA.xoo', update this property so that patterns refer to project-relative paths."); + + List<Issue> issues = result.issuesFor(result.inputFile("moduleA/src/sampleA.xoo")); + assertThat(issues).isEmpty(); + + issues = result.issuesFor(result.inputFile("moduleB/src/sampleB.xoo")); + assertThat(issues).hasSize(10); + } + + @Test + public void warn_user_for_unsupported_module_level_IssueExclusion() throws Exception { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + FileUtils.write(xooFileA, "1\n2\n3\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + File xooFileB = new File(srcDirB, "sampleB.xoo"); + FileUtils.write(xooFileB, "1\n2\n3\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.sources", "src") + .put("sonar.scm.disabled", "true") + .put("sonar.issue.ignore.multicriteria", "1") + .put("sonar.issue.ignore.multicriteria.1.ruleKey", "*") + .put("sonar.issue.ignore.multicriteria.1.resourceKey", "*") + .build()) + .execute(); + + assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); + + result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.sources", "src") + .put("sonar.scm.disabled", "true") + .put("moduleA.sonar.issue.ignore.multicriteria", "1") + .put("moduleA.sonar.issue.ignore.multicriteria.1.ruleKey", "*") + .put("moduleA.sonar.issue.ignore.multicriteria.1.resourceKey", "*") + .build()) + .execute(); + + assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly( + "Specifying issue exclusions at module level is not supported anymore. Configure the property 'sonar.issue.ignore.multicriteria' and any other issue exclusions at project level."); + + List<Issue> issues = result.issuesFor(result.inputFile("moduleA/src/sampleA.xoo")); + assertThat(issues).hasSize(10); + + issues = result.issuesFor(result.inputFile("moduleB/src/sampleB.xoo")); + assertThat(issues).hasSize(10); + + // SONAR-11850 The Maven scanner replicates properties defined on the root module to all modules + logTester.clear(); + result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.sources", "src") + .put("sonar.scm.disabled", "true") + .put("sonar.issue.ignore.multicriteria", "1") + .put("sonar.issue.ignore.multicriteria.1.ruleKey", "*") + .put("sonar.issue.ignore.multicriteria.1.resourceKey", "*") + .put("moduleA.sonar.issue.ignore.multicriteria", "1") + .put("moduleA.sonar.issue.ignore.multicriteria.1.ruleKey", "*") + .put("moduleA.sonar.issue.ignore.multicriteria.1.resourceKey", "*") + .build()) + .execute(); + + assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); + } + + @Test + public void testIssueExclusionByEnforceMultiCriteria() throws Exception { + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + activateTODORule(); + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, "1\n2\n3 TODO\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + File xooFile11 = new File(srcDir, "sample11.xoo"); + FileUtils.write(xooFile11, "1\n2\n3 TODO\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .property("sonar.issue.enforce.multicriteria", "1,2") + .property("sonar.issue.enforce.multicriteria.1.ruleKey", "xoo:HasTag") + .property("sonar.issue.enforce.multicriteria.1.resourceKey", "src/sample11.xoo") + .property("sonar.issue.enforce.multicriteria.2.ruleKey", "xoo:One*") + .property("sonar.issue.enforce.multicriteria.2.resourceKey", "src/sample?.xoo") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("src/sample1.xoo")); + assertThat(issues).hasSize(10); + + issues = result.issuesFor(result.inputFile("src/sample11.xoo")); + assertThat(issues).hasSize(2); + } + + @Test + public void warn_user_for_outdated_IssueExclusionByEnforceMultiCriteria() throws Exception { + File baseDir = temp.getRoot(); + File baseDirModuleA = new File(baseDir, "moduleA"); + File baseDirModuleB = new File(baseDir, "moduleB"); + File srcDirA = new File(baseDirModuleA, "src"); + srcDirA.mkdirs(); + File srcDirB = new File(baseDirModuleB, "src"); + srcDirB.mkdirs(); + + File xooFileA = new File(srcDirA, "sampleA.xoo"); + FileUtils.write(xooFileA, "1\n2\n3\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + File xooFileB = new File(srcDirB, "sampleB.xoo"); + FileUtils.write(xooFileB, "1\n2\n3\n4\n5\n6 TODO\n7\n8\n9\n10", StandardCharsets.UTF_8); + + tester + .addProjectServerSettings("sonar.issue.enforce.multicriteria", "1") + .addProjectServerSettings("sonar.issue.enforce.multicriteria.1.ruleKey", "*") + .addProjectServerSettings("sonar.issue.enforce.multicriteria.1.resourceKey", "src/sampleA.xoo"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "moduleA,moduleB") + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(logTester.logs(LoggerLevel.WARN)).contains( + "Specifying module-relative paths at project level in property 'sonar.issue.enforce.multicriteria' is deprecated. To continue matching files like 'moduleA/src/sampleA.xoo', update this property so that patterns refer to project-relative paths."); + + List<Issue> issues = result.issuesFor(result.inputFile("moduleA/src/sampleA.xoo")); + assertThat(issues).hasSize(10); + + issues = result.issuesFor(result.inputFile("moduleB/src/sampleB.xoo")); + assertThat(issues).isEmpty(); + } + + private void activateTODORule() { + LoadedActiveRule r = new LoadedActiveRule(); + r.setRuleKey(RuleKey.of("xoo", HasTagSensor.RULE_KEY)); + r.setName("TODO"); + r.setLanguage("xoo"); + r.setSeverity("MAJOR"); + r.setDeprecatedKeys(emptySet() + ); + r.setParams(ImmutableMap.of("tag", "TODO")); + tester.activateRule(r); + } + + @Test + public void testIssueDetails() throws IOException { + File baseDir = temp.newFolder(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("src/sample.xoo")); + assertThat(issues).hasSize(10); + assertThat(issues) + .extracting("msg", "textRange.startLine", "gap") + .contains(tuple("This issue is generated on each line", 1, 0.0)); + } + + @Test + public void testIssueFilter() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample"); + File tmpDir = temp.newFolder(); + FileUtils.copyDirectory(projectDir, tmpDir); + + AnalysisResult result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .property("sonar.xoo.excludeAllIssuesOnOddLines", "true") + .execute(); + + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); + assertThat(issues).hasSize(4 /* even lines */); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesOnDirMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesOnDirMediumIT.java new file mode 100644 index 00000000000..80553376649 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesOnDirMediumIT.java @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IssuesOnDirMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssueOnDirPerFile", null, "One issue per line", "MINOR", "xoo", "xoo"); + + @Test + public void scanTempProject() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, "Sample1 xoo\ncontent"); + + File xooFile2 = new File(srcDir, "sample2.xoo"); + FileUtils.write(xooFile2, "Sample2 xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.issuesFor(result.project())).hasSize(2); + } + + @Test + public void issueOnRootFolder() throws IOException { + + File baseDir = temp.getRoot(); + + File xooFile1 = new File(baseDir, "sample1.xoo"); + FileUtils.write(xooFile1, "Sample1 xoo\ncontent"); + + File xooFile2 = new File(baseDir, "sample2.xoo"); + FileUtils.write(xooFile2, "Sample2 xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", ".") + .build()) + .execute(); + + assertThat(result.issuesFor(result.project())).hasSize(2); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesOnModuleMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesOnModuleMediumIT.java new file mode 100644 index 00000000000..b83481d5729 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/IssuesOnModuleMediumIT.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IssuesOnModuleMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRules(new XooRulesDefinition()) + .addActiveRule("xoo", "OneIssuePerModule", null, "One issue per module", "MINOR", "xoo", "xoo"); + + @Test + public void scanTempProject() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile1 = new File(srcDir, "sample1.xoo"); + FileUtils.write(xooFile1, "Sample1 xoo\ncontent"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()) + .execute(); + + assertThat(result.issuesFor(result.project())).hasSize(1); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/MultilineIssuesMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/MultilineIssuesMediumIT.java new file mode 100644 index 00000000000..5d69b83c7c8 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/MultilineIssuesMediumIT.java @@ -0,0 +1,137 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import java.io.File; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport.Flow; +import org.sonar.scanner.protocol.output.ScannerReport.FlowType; +import org.sonar.scanner.protocol.output.ScannerReport.Issue; +import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class MultilineIssuesMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addRules(new XooRulesDefinition()) + .addDefaultQProfile("xoo", "Sonar Way") + .addActiveRule("xoo", "MultilineIssue", null, "Multinile Issue", "MAJOR", null, "xoo"); + + private AnalysisResult result; + + @Before + public void prepare() throws Exception { + File projectDir = new File("test-resources/mediumtest/xoo/sample-multiline"); + File tmpDir = temp.getRoot(); + FileUtils.copyDirectory(projectDir, tmpDir); + + result = tester + .newAnalysis(new File(tmpDir, "sonar-project.properties")) + .execute(); + } + + @Test + public void testIssueRange() { + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/Single.xoo")); + assertThat(issues).hasSize(1); + Issue issue = issues.get(0); + assertThat(issue.getMsg()).isEqualTo("Primary location of the issue in xoo code"); + assertThat(issue.getTextRange().getStartLine()).isEqualTo(6); + assertThat(issue.getTextRange().getStartOffset()).isEqualTo(23); + assertThat(issue.getTextRange().getEndLine()).isEqualTo(6); + assertThat(issue.getTextRange().getEndOffset()).isEqualTo(50); + } + + @Test + public void testMultilineIssueRange() { + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/Multiline.xoo")); + assertThat(issues).hasSize(1); + Issue issue = issues.get(0); + assertThat(issue.getMsg()).isEqualTo("Primary location of the issue in xoo code"); + assertThat(issue.getTextRange().getStartLine()).isEqualTo(6); + assertThat(issue.getTextRange().getStartOffset()).isEqualTo(23); + assertThat(issue.getTextRange().getEndLine()).isEqualTo(7); + assertThat(issue.getTextRange().getEndOffset()).isEqualTo(23); + } + + @Test + public void testFlowWithSingleLocation() { + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/Multiple.xoo")); + assertThat(issues).hasSize(1); + Issue issue = issues.get(0); + assertThat(issue.getMsg()).isEqualTo("Primary location of the issue in xoo code"); + assertThat(issue.getTextRange().getStartLine()).isEqualTo(6); + assertThat(issue.getTextRange().getStartOffset()).isEqualTo(23); + assertThat(issue.getTextRange().getEndLine()).isEqualTo(6); + assertThat(issue.getTextRange().getEndOffset()).isEqualTo(50); + + assertThat(issue.getFlowList()).hasSize(1); + Flow flow = issue.getFlow(0); + assertThat(flow.getLocationList()).hasSize(1); + IssueLocation additionalLocation = flow.getLocation(0); + assertThat(additionalLocation.getMsg()).isEqualTo("Xoo code, flow step #1"); + assertThat(additionalLocation.getTextRange().getStartLine()).isEqualTo(7); + assertThat(additionalLocation.getTextRange().getStartOffset()).isEqualTo(26); + assertThat(additionalLocation.getTextRange().getEndLine()).isEqualTo(7); + assertThat(additionalLocation.getTextRange().getEndOffset()).isEqualTo(53); + } + + @Test + public void testFlowsWithMultipleElements() { + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/WithFlow.xoo")); + assertThat(issues).hasSize(1); + Issue issue = issues.get(0); + assertThat(issue.getFlowList()).hasSize(1); + + Flow flow = issue.getFlow(0); + assertThat(flow.getLocationList()).hasSize(2); + // TODO more assertions + } + + @Test + public void testFlowsWithTypes() { + List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/FlowTypes.xoo")); + assertThat(issues).hasSize(1); + Issue issue = issues.get(0); + assertThat(issue.getFlowList()).hasSize(3); + + assertThat(issue.getFlowList()).extracting(Flow::getType, Flow::getDescription, f -> f.getLocationList().size()) + .containsExactly( + tuple(FlowType.DATA, "flow #1", 1), + tuple(FlowType.UNDEFINED, "", 1), + tuple(FlowType.EXECUTION, "flow #3", 1)); + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/PreviewMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/PreviewMediumIT.java new file mode 100644 index 00000000000..6cd81792a93 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/issues/PreviewMediumIT.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.issues; + +import com.google.common.collect.ImmutableMap; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.log.LogTester; +import org.sonar.scanner.mediumtest.ScannerMediumTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class PreviewMediumIT { + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester(); + + @Test + public void failWhenUsingPreviewMode() { + try { + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.analysis.mode", "preview").build()) + .execute(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).isInstanceOf(MessageException.class).hasMessage("The preview mode, along with the 'sonar.analysis.mode' parameter, is no more supported. You should stop using this parameter."); + } + } + + @Test + public void failWhenUsingIssuesMode() { + try { + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.analysis.mode", "issues").build()) + .execute(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).isInstanceOf(MessageException.class).hasMessage("The preview mode, along with the 'sonar.analysis.mode' parameter, is no more supported. You should stop using this parameter."); + } + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/log/ExceptionHandlingMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/log/ExceptionHandlingMediumIT.java new file mode 100644 index 00000000000..64c41253437 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/log/ExceptionHandlingMediumIT.java @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.log; + +import java.util.Collections; +import java.util.Map; +import javax.annotation.Priority; +import org.junit.BeforeClass; +import org.junit.Test; +import org.sonar.api.utils.MessageException; +import org.sonar.batch.bootstrapper.Batch; +import org.sonar.batch.bootstrapper.EnvironmentInformation; +import org.sonar.scanner.repository.settings.GlobalSettingsLoader; +import org.springframework.beans.factory.UnsatisfiedDependencyException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ExceptionHandlingMediumIT { + + private Batch batch; + private static ErrorGlobalSettingsLoader loader; + + @BeforeClass + public static void beforeClass() { + loader = new ErrorGlobalSettingsLoader(); + } + + public void setUp(boolean verbose) { + Batch.Builder builder = Batch.builder() + .setEnableLoggingConfiguration(true) + .addComponents( + loader, + new EnvironmentInformation("mediumTest", "1.0")); + + if (verbose) { + builder.setGlobalProperties(Collections.singletonMap("sonar.verbose", "true")); + } + batch = builder.build(); + } + + @Test + public void test() throws Exception { + setUp(false); + loader.withCause = false; + + assertThatThrownBy(() -> batch.execute()) + .isInstanceOf(MessageException.class) + .hasMessage("Error loading settings"); + } + + @Test + public void testWithCause() throws Exception { + setUp(false); + loader.withCause = true; + + assertThatThrownBy(() -> batch.execute()) + .isInstanceOf(MessageException.class) + .hasMessage("Error loading settings") + .hasCauseInstanceOf(Throwable.class) + .hasRootCauseMessage("Code 401"); + } + + @Test + public void testWithVerbose() { + setUp(true); + assertThatThrownBy(() -> batch.execute()) + .isInstanceOf(UnsatisfiedDependencyException.class) + .hasMessageContaining("Error loading settings"); + } + + @Priority(1) + private static class ErrorGlobalSettingsLoader implements GlobalSettingsLoader { + boolean withCause = false; + + @Override + public Map<String, String> loadGlobalSettings() { + if (withCause) { + IllegalStateException cause = new IllegalStateException("Code 401"); + throw MessageException.of("Error loading settings", cause); + } else { + throw MessageException.of("Error loading settings"); + } + } + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/log/LogListenerIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/log/LogListenerIT.java new file mode 100644 index 00000000000..e2a63e2ac6c --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/log/LogListenerIT.java @@ -0,0 +1,256 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.log; + +import com.google.common.collect.ImmutableMap; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.batch.bootstrapper.LogOutput; +import org.sonar.batch.bootstrapper.LogOutput.Level; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class LogListenerIT { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private Pattern simpleTimePattern = Pattern.compile("\\d{2}:\\d{2}:\\d{2}"); + private List<LogEvent> logOutput; + private StringBuilder logOutputStr; + private ByteArrayOutputStream stdOutTarget; + private ByteArrayOutputStream stdErrTarget; + private static PrintStream savedStdOut; + private static PrintStream savedStdErr; + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .setLogOutput(new SimpleLogListener()); + + private File baseDir; + private ImmutableMap.Builder<String, String> builder; + + @BeforeClass + public static void backupStdStreams() { + savedStdOut = System.out; + savedStdErr = System.err; + } + + @AfterClass + public static void resumeStdStreams() { + if (savedStdOut != null) { + System.setOut(savedStdOut); + } + if (savedStdErr != null) { + System.setErr(savedStdErr); + } + } + + @Before + public void prepare() { + System.out.flush(); + System.err.flush(); + stdOutTarget = new ByteArrayOutputStream(); + stdErrTarget = new ByteArrayOutputStream(); + System.setOut(new PrintStream(stdOutTarget)); + System.setErr(new PrintStream(stdErrTarget)); + // logger from the batch might write to it asynchronously + logOutput = Collections.synchronizedList(new LinkedList<>()); + logOutputStr = new StringBuilder(); + + baseDir = temp.getRoot(); + + builder = ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project"); + } + + private void assertNoStdOutput() { + assertThat(new String(stdOutTarget.toByteArray())).isEmpty(); + assertThat(new String(stdErrTarget.toByteArray())).isEmpty(); + } + + /** + * Check that log message is not formatted, i.e. has no log level and timestamp. + */ + private void assertMsgClean(String msg) { + // FP: [JOURNAL_FLUSHER] WARNING Journal flush operation took 2,093ms last 8 cycles average is 262ms + if (msg.contains("[JOURNAL_FLUSHER]")) { + return; + } + + for (Level l : Level.values()) { + assertThat(msg).doesNotContain(l.toString()); + } + + Matcher matcher = simpleTimePattern.matcher(msg); + assertThat(matcher.find()).isFalse(); + } + + @Test + public void testChangeLogForAnalysis() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\ncontent", StandardCharsets.UTF_8); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .put("sonar.verbose", "true") + .build()) + .execute(); + + synchronized (logOutput) { + for (LogEvent e : logOutput) { + savedStdOut.println("[captured]" + e.level + " " + e.msg); + } + } + + // only done in DEBUG during analysis + assertThat(logOutputStr.toString()).contains("Post-jobs : "); + } + + @Test + public void testNoStdLog() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\ncontent", StandardCharsets.UTF_8); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertNoStdOutput(); + assertThat(logOutput).isNotEmpty(); + + synchronized (logOutput) { + for (LogEvent e : logOutput) { + savedStdOut.println("[captured]" + e.level + " " + e.msg); + } + } + } + + @Test + public void testNoFormattedMsgs() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\ncontent", StandardCharsets.UTF_8); + + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + + assertNoStdOutput(); + + synchronized (logOutput) { + for (LogEvent e : logOutput) { + assertMsgClean(e.msg); + savedStdOut.println("[captured]" + e.level + " " + e.msg); + } + } + } + + // SONAR-7540 + @Test + public void testStackTrace() throws IOException { + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\ncontent"); + File xooFileMeasure = new File(srcDir, "sample.xoo.measures"); + FileUtils.write(xooFileMeasure, "foo:bar", StandardCharsets.UTF_8); + + try { + tester.newAnalysis() + .properties(builder + .put("sonar.sources", "src") + .build()) + .execute(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e.getMessage()).contains("Error processing line 1"); + } + + assertNoStdOutput(); + + synchronized (logOutput) { + for (LogEvent e : logOutput) { + if (e.level == Level.ERROR) { + assertThat(e.msg).contains("Error processing line 1 of file", "src" + File.separator + "sample.xoo.measures", + "java.lang.IllegalStateException: Unknown metric with key: foo", + "at org.sonar.xoo.lang.MeasureSensor.saveMeasure"); + + } + } + } + } + + private class SimpleLogListener implements LogOutput { + @Override + public void log(String msg, Level level) { + logOutput.add(new LogEvent(msg, level)); + logOutputStr.append(msg).append("\n"); + } + } + + private static class LogEvent { + String msg; + Level level; + + LogEvent(String msg, Level level) { + this.msg = msg; + this.level = level; + } + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/measures/MeasuresMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/measures/MeasuresMediumIT.java new file mode 100644 index 00000000000..c3f4fb1768e --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/measures/MeasuresMediumIT.java @@ -0,0 +1,182 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.measures; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport.Measure; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.Assert.fail; + +public class MeasuresMediumIT { + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private File baseDir; + private File srcDir; + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Before + public void setUp() throws Exception { + baseDir = temp.newFolder(); + srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + } + + @Test + public void failIfTryingToSaveServerSideMeasure() throws IOException { + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\n\ncontent", StandardCharsets.UTF_8); + + File measures = new File(srcDir, "sample.xoo.measures"); + FileUtils.write(measures, "new_lines:2", StandardCharsets.UTF_8); + + try { + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e) + .hasCauseInstanceOf(UnsupportedOperationException.class) + .hasStackTraceContaining("Metric 'new_lines' should not be computed by a Sensor"); + } + } + + @Test + public void lineMeasures() throws IOException { + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\n\n\ncontent", StandardCharsets.UTF_8); + + File lineMeasures = new File(srcDir, "sample.xoo.linemeasures"); + FileUtils.write(lineMeasures, "ncloc_data:1=1;2=0;4=1", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + Map<String, List<Measure>> allMeasures = result.allMeasures(); + + assertThat(allMeasures.get("com.foo.project:src/sample.xoo")).extracting("metricKey", "intValue.value", "stringValue.value") + .containsExactly(tuple("ncloc_data", 0, "1=1;4=1")); + } + + @Test + public void projectLevelMeasures() throws IOException { + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\n\n\ncontent", StandardCharsets.UTF_8); + + File projectMeasures = new File(baseDir, "module.measures"); + FileUtils.write(projectMeasures, "tests:10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + Map<String, List<Measure>> allMeasures = result.allMeasures(); + + assertThat(allMeasures.get("com.foo.project")) + .extracting("metricKey", "intValue.value", "stringValue.value") + .containsExactly(tuple("tests", 10, "")); + } + + @Test + public void warnWhenSavingFolderMeasure() throws IOException { + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\n\n\ncontent", StandardCharsets.UTF_8); + + File folderMeasures = new File(srcDir, "folder.measures"); + FileUtils.write(folderMeasures, "tests:10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Storing measures on folders or modules is deprecated. Provided value of metric 'tests' is ignored."); + } + + @Test + public void warnWhenSavingModuleMeasure() throws IOException { + File moduleDir = new File(baseDir, "moduleA"); + moduleDir.mkdirs(); + + srcDir = new File(moduleDir, "src"); + + File xooFile = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile, "Sample xoo\n\n\ncontent", StandardCharsets.UTF_8); + + File moduleMeasures = new File(moduleDir, "module.measures"); + FileUtils.write(moduleMeasures, "tests:10", StandardCharsets.UTF_8); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.modules", "moduleA") + .put("sonar.sources", "src") + .build()) + .execute(); + + + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Storing measures on folders or modules is deprecated. Provided value of metric 'tests' is ignored."); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java new file mode 100644 index 00000000000..04451bf92f0 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/scm/ScmMediumIT.java @@ -0,0 +1,358 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.scm; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.assertj.core.util.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.SonarEdition; +import org.sonar.api.utils.log.LogTester; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.mediumtest.ScannerMediumTester.AnalysisBuilder; +import org.sonar.scanner.protocol.output.FileStructure; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.scanner.protocol.output.ScannerReport.Changesets.Changeset; +import org.sonar.scanner.protocol.output.ScannerReport.Component; +import org.sonar.scanner.protocol.output.ScannerReportReader; +import org.sonar.scanner.repository.FileData; +import org.sonar.xoo.XooPlugin; +import org.sonar.xoo.rule.XooRulesDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScmMediumIT { + + private static final String MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES = "Missing blame information for the following files:"; + private static final String CHANGED_CONTENT_SCM_ON_SERVER_XOO = "src/changed_content_scm_on_server.xoo"; + private static final String NO_BLAME_SCM_ON_SERVER_XOO = "src/no_blame_scm_on_server.xoo"; + private static final String SAME_CONTENT_SCM_ON_SERVER_XOO = "src/same_content_scm_on_server.xoo"; + private static final String SAME_CONTENT_NO_SCM_ON_SERVER_XOO = "src/same_content_no_scm_on_server.xoo"; + private static final String SAMPLE_XOO_CONTENT = "Sample xoo\ncontent"; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .setEdition(SonarEdition.COMMUNITY) + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way") + .addRules(new XooRulesDefinition()) + // active a rule just to be sure that xoo files are published + .addActiveRule("xoo", "xoo:OneIssuePerFile", null, "One Issue Per File", null, null, null) + .addFileData(CHANGED_CONTENT_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), null)) + .addFileData(SAME_CONTENT_NO_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), null)) + .addFileData(SAME_CONTENT_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), "1.1")) + .addFileData(NO_BLAME_SCM_ON_SERVER_XOO, new FileData(DigestUtils.md5Hex(SAMPLE_XOO_CONTENT), "1.1")); + + @Test + public void testScmMeasure() throws IOException, URISyntaxException { + File baseDir = prepareProject(); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.scm.provider", "xoo") + .build()) + .execute(); + + ScannerReport.Changesets fileScm = getChangesets(baseDir, "src/sample.xoo"); + + assertThat(fileScm.getChangesetIndexByLineList()).hasSize(5); + + Changeset changesetLine1 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(0)); + assertThat(changesetLine1.getAuthor()).isEmpty(); + + Changeset changesetLine2 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(1)); + assertThat(changesetLine2.getAuthor()).isEqualTo(getNonAsciiAuthor().toLowerCase()); + + Changeset changesetLine3 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(2)); + assertThat(changesetLine3.getAuthor()).isEqualTo("julien"); + + Changeset changesetLine4 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(3)); + assertThat(changesetLine4.getAuthor()).isEqualTo("julien"); + + Changeset changesetLine5 = fileScm.getChangeset(fileScm.getChangesetIndexByLine(4)); + assertThat(changesetLine5.getAuthor()).isEqualTo("simon"); + } + + private ScannerReport.Changesets getChangesets(File baseDir, String path) { + File reportDir = new File(baseDir, ".sonar/scanner-report"); + FileStructure fileStructure = new FileStructure(reportDir); + ScannerReportReader reader = new ScannerReportReader(fileStructure); + + Component project = reader.readComponent(reader.readMetadata().getRootComponentRef()); + for (Integer fileRef : project.getChildRefList()) { + Component file = reader.readComponent(fileRef); + if (file.getProjectRelativePath().equals(path)) { + return reader.readChangesets(file.getRef()); + } + } + return null; + } + + @Test + public void noScmOnEmptyFile() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + + // Clear file content + FileUtils.write(new File(baseDir, "src/sample.xoo"), ""); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .put("sonar.scm.provider", "xoo") + .build()) + .execute(); + + ScannerReport.Changesets changesets = getChangesets(baseDir, "src/sample.xoo"); + + assertThat(changesets).isNull(); + } + + @Test + public void log_files_with_missing_blame() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + File xooFileWithoutBlame = new File(baseDir, "src/sample_no_blame.xoo"); + FileUtils.write(xooFileWithoutBlame, "Sample xoo\ncontent\n3\n4\n5", StandardCharsets.UTF_8); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .put("sonar.scm.provider", "xoo") + .build()) + .execute(); + + ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo"); + assertThat(file1Scm).isNotNull(); + + ScannerReport.Changesets fileWithoutBlameScm = getChangesets(baseDir, "src/sample_no_blame.xoo"); + assertThat(fileWithoutBlameScm).isNull(); + + assertThat(logTester.logs()).containsSubsequence("SCM Publisher 2 source files to be analyzed", MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES, + " * src/sample_no_blame.xoo"); + + assertThat(logTester.logs().stream().anyMatch(s -> Pattern.matches("SCM Publisher 1/2 source file have been analyzed \\(done\\) \\| time=[0-9]+ms", s))).isTrue(); + } + + // SONAR-6397 + @Test + public void optimize_blame() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + File changedContentScmOnServer = new File(baseDir, CHANGED_CONTENT_SCM_ON_SERVER_XOO); + FileUtils.write(changedContentScmOnServer, SAMPLE_XOO_CONTENT + "\nchanged", StandardCharsets.UTF_8); + File xooScmFile = new File(baseDir, CHANGED_CONTENT_SCM_ON_SERVER_XOO + ".scm"); + FileUtils.write(xooScmFile, + // revision,author,dateTime + "1,foo,2013-01-04\n" + + "1,bar,2013-01-04\n" + + "2,biz,2014-01-04\n", + StandardCharsets.UTF_8); + + File sameContentScmOnServer = new File(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO); + FileUtils.write(sameContentScmOnServer, SAMPLE_XOO_CONTENT, StandardCharsets.UTF_8); + // No need to write .scm file since this file should not be blamed + + File noBlameScmOnServer = new File(baseDir, NO_BLAME_SCM_ON_SERVER_XOO); + FileUtils.write(noBlameScmOnServer, SAMPLE_XOO_CONTENT + "\nchanged", StandardCharsets.UTF_8); + // No .scm file to emulate a failure during blame + + File sameContentNoScmOnServer = new File(baseDir, SAME_CONTENT_NO_SCM_ON_SERVER_XOO); + FileUtils.write(sameContentNoScmOnServer, SAMPLE_XOO_CONTENT, StandardCharsets.UTF_8); + xooScmFile = new File(baseDir, SAME_CONTENT_NO_SCM_ON_SERVER_XOO + ".scm"); + FileUtils.write(xooScmFile, + // revision,author,dateTime + "1,foo,2013-01-04\n" + + "1,bar,2013-01-04\n", + StandardCharsets.UTF_8); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .put("sonar.scm.provider", "xoo") + .build()) + .execute(); + + assertThat(getChangesets(baseDir, "src/sample.xoo")).isNotNull(); + + assertThat(getChangesets(baseDir, CHANGED_CONTENT_SCM_ON_SERVER_XOO).getCopyFromPrevious()).isFalse(); + + assertThat(getChangesets(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO).getCopyFromPrevious()).isTrue(); + + assertThat(getChangesets(baseDir, SAME_CONTENT_NO_SCM_ON_SERVER_XOO).getCopyFromPrevious()).isFalse(); + + assertThat(getChangesets(baseDir, NO_BLAME_SCM_ON_SERVER_XOO)).isNull(); + + // 5 .xoo files + 3 .scm files, but only 4 marked for publishing. 1 file is SAME so not included in the total + assertThat(logTester.logs()).containsSubsequence("8 files indexed"); + assertThat(logTester.logs()).containsSubsequence("SCM Publisher 4 source files to be analyzed"); + assertThat(logTester.logs().stream().anyMatch(s -> Pattern.matches("SCM Publisher 3/4 source files have been analyzed \\(done\\) \\| time=[0-9]+ms", s))).isTrue(); + assertThat(logTester.logs()).containsSubsequence(MISSING_BLAME_INFORMATION_FOR_THE_FOLLOWING_FILES, " * src/no_blame_scm_on_server.xoo"); + } + + @Test + public void forceReload() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + File xooFileNoScm = new File(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO); + FileUtils.write(xooFileNoScm, SAMPLE_XOO_CONTENT, StandardCharsets.UTF_8); + File xooScmFile = new File(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO + ".scm"); + FileUtils.write(xooScmFile, + // revision,author,dateTime + "1,foo,2013-01-04\n" + + "1,bar,2013-01-04\n", + StandardCharsets.UTF_8); + + AnalysisBuilder analysisBuilder = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.scm.provider", "xoo") + // Force reload + .put("sonar.scm.forceReloadAll", "true") + .build()); + + analysisBuilder.execute(); + + ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo"); + assertThat(file1Scm).isNotNull(); + + ScannerReport.Changesets file2Scm = getChangesets(baseDir, SAME_CONTENT_SCM_ON_SERVER_XOO); + assertThat(file2Scm).isNotNull(); + } + + @Test + public void configureUsingScmURL() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.links.scm_dev", "scm:xoo:foobar") + .build()) + .execute(); + + ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo"); + assertThat(file1Scm).isNotNull(); + } + + @Test + public void testAutoDetection() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + new File(baseDir, ".xoo").createNewFile(); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .build()) + .execute(); + + ScannerReport.Changesets file1Scm = getChangesets(baseDir, "src/sample.xoo"); + assertThat(file1Scm).isNotNull(); + } + + private String getNonAsciiAuthor() { + return Files.contentOf(new File("test-resources/mediumtest/blameAuthor.txt"), StandardCharsets.UTF_8); + + } + + private File prepareProject() throws IOException { + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile1 = new File(srcDir, "sample.xoo"); + FileUtils.write(xooFile1, "Sample xoo\ncontent\n3\n4\n5", StandardCharsets.UTF_8); + File xooScmFile1 = new File(srcDir, "sample.xoo.scm"); + FileUtils.write(xooScmFile1, + // revision,author,dateTime + "1,,2013-01-04\n" + + "2," + getNonAsciiAuthor() + ",2013-01-04\n" + + "3,julien,2013-02-03\n" + + "3,julien,2013-02-03\n" + + "4,simon,2013-03-04\n", + StandardCharsets.UTF_8); + + return baseDir; + } + + @Test + public void testDisableScmSensor() throws IOException, URISyntaxException { + + File baseDir = prepareProject(); + + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.sources", "src") + .put("sonar.scm.disabled", "true") + .put("sonar.scm.provider", "xoo") + .put("sonar.cpd.xoo.skip", "true") + .build()) + .execute(); + + ScannerReport.Changesets changesets = getChangesets(baseDir, "src/sample.xoo"); + assertThat(changesets).isNull(); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/symbol/SymbolMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/symbol/SymbolMediumIT.java new file mode 100644 index 00000000000..f6f535ac1d7 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/symbol/SymbolMediumIT.java @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.symbol; + +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SymbolMediumIT { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Test + public void computeSymbolReferencesOnTempProject() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File xooSymbolFile = new File(srcDir, "sample.xoo.symbol"); + FileUtils.write(xooFile, "Sample xoo\ncontent\nanother xoo"); + // Highlight xoo symbol + FileUtils.write(xooSymbolFile, "1:7:1:10,3:8:3:11"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.symbolReferencesFor(file, 1, 7)).containsOnly(ScannerReport.TextRange.newBuilder().setStartLine(3).setStartOffset(8).setEndLine(3).setEndOffset(11).build()); + } + + @Test + public void computeSymbolReferencesWithVariableLength() throws IOException { + + File baseDir = temp.getRoot(); + File srcDir = new File(baseDir, "src"); + srcDir.mkdir(); + + File xooFile = new File(srcDir, "sample.xoo"); + File xooSymbolFile = new File(srcDir, "sample.xoo.symbol"); + FileUtils.write(xooFile, "Sample xoo\ncontent\nanother xoo\nyet another"); + // Highlight xoo symbol + FileUtils.write(xooSymbolFile, "1:7:1:10,3:8:4:1"); + + AnalysisResult result = tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "scan") + .put("sonar.projectBaseDir", baseDir.getAbsolutePath()) + .put("sonar.projectKey", "com.foo.project") + .put("sonar.projectName", "Foo Project") + .put("sonar.projectVersion", "1.0-SNAPSHOT") + .put("sonar.projectDescription", "Description of Foo Project") + .put("sonar.sources", "src") + .build()) + .execute(); + + InputFile file = result.inputFile("src/sample.xoo"); + assertThat(result.symbolReferencesFor(file, 1, 7)).containsOnly(ScannerReport.TextRange.newBuilder().setStartLine(3).setStartOffset(8).setEndLine(4).setEndOffset(1).build()); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/tasks/TasksMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/tasks/TasksMediumIT.java new file mode 100644 index 00000000000..776a11575bf --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/tasks/TasksMediumIT.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.tasks; + +import com.google.common.collect.ImmutableMap; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.Plugin; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.log.LogTester; +import org.sonar.scanner.mediumtest.ScannerMediumTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class TasksMediumIT { + + @Rule + public LogTester logTester = new LogTester(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("faketask", new FakePlugin()); + + @Test + public void failWhenCallingTask() { + try { + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "fake").build()) + .execute(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).isInstanceOf(MessageException.class).hasMessage("Tasks support was removed in SonarQube 7.6."); + } + } + + @Test + public void failWhenCallingViews() { + try { + tester.newAnalysis() + .properties(ImmutableMap.<String, String>builder() + .put("sonar.task", "views").build()) + .execute(); + fail("Expected exception"); + } catch (Exception e) { + assertThat(e).isInstanceOf(MessageException.class).hasMessage("The task 'views' was removed with SonarQube 7.1. You can safely remove this call since portfolios and applications are automatically re-calculated."); + } + } + + private static class FakePlugin implements Plugin { + + @Override + public void define(Context context) { + } + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/tests/GenericTestExecutionMediumIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/tests/GenericTestExecutionMediumIT.java new file mode 100644 index 00000000000..86962f74824 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scanner/mediumtest/tests/GenericTestExecutionMediumIT.java @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scanner.mediumtest.tests; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.scanner.mediumtest.AnalysisResult; +import org.sonar.scanner.mediumtest.ScannerMediumTester; +import org.sonar.xoo.XooPlugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class GenericTestExecutionMediumIT { + private final List<String> logs = new ArrayList<>(); + + @Rule + public ScannerMediumTester tester = new ScannerMediumTester() + .registerPlugin("xoo", new XooPlugin()) + .addDefaultQProfile("xoo", "Sonar Way"); + + @Test + public void singleReport() { + + File projectDir = new File("test-resources/mediumtest/xoo/sample-generic-test-exec"); + + AnalysisResult result = tester + .setLogOutput((msg, level) -> logs.add(msg)) + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.testExecutionReportPaths", "unittest.xml") + .execute(); + + InputFile testFile = result.inputFile("testx/ClassOneTest.xoo"); + assertThat(result.allMeasures().get(testFile.key())).extracting("metricKey", "intValue.value", "longValue.value") + .containsOnly( + tuple(CoreMetrics.TESTS_KEY, 3, 0L), + tuple(CoreMetrics.SKIPPED_TESTS_KEY, 1, 0L), + tuple(CoreMetrics.TEST_ERRORS_KEY, 1, 0L), + tuple(CoreMetrics.TEST_EXECUTION_TIME_KEY, 0, 1105L), + tuple(CoreMetrics.TEST_FAILURES_KEY, 1, 0L)); + + assertThat(logs).noneMatch(l -> l.contains("Please use 'sonar.testExecutionReportPaths'")); + } + + @Test + public void twoReports() { + + File projectDir = new File("test-resources/mediumtest/xoo/sample-generic-test-exec"); + + AnalysisResult result = tester + .setLogOutput((msg, level) -> logs.add(msg)) + .newAnalysis(new File(projectDir, "sonar-project.properties")) + .property("sonar.testExecutionReportPaths", "unittest.xml,unittest2.xml") + .execute(); + + InputFile testFile = result.inputFile("testx/ClassOneTest.xoo"); + assertThat(result.allMeasures().get(testFile.key())).extracting("metricKey", "intValue.value", "longValue.value") + .containsOnly( + tuple(CoreMetrics.TESTS_KEY, 4, 0L), + tuple(CoreMetrics.SKIPPED_TESTS_KEY, 2, 0L), + tuple(CoreMetrics.TEST_ERRORS_KEY, 1, 0L), + tuple(CoreMetrics.TEST_EXECUTION_TIME_KEY, 0, 1610L), + tuple(CoreMetrics.TEST_FAILURES_KEY, 1, 0L)); + + assertThat(logs).noneMatch(l -> l.contains("Please use 'sonar.testExecutionReportPaths'")); + } + +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnBlameCommandIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnBlameCommandIT.java new file mode 100644 index 00000000000..dec6960b3cb --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnBlameCommandIT.java @@ -0,0 +1,391 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scm.svn; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.stream.IntStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.ArgumentCaptor; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; +import org.sonar.api.batch.scm.BlameCommand.BlameInput; +import org.sonar.api.batch.scm.BlameCommand.BlameOutput; +import org.sonar.api.batch.scm.BlameLine; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.tmatesoft.svn.core.SVNAuthenticationException; +import org.tmatesoft.svn.core.SVNDepth; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; +import org.tmatesoft.svn.core.internal.wc2.compat.SvnCodec; +import org.tmatesoft.svn.core.wc.ISVNOptions; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNLogClient; +import org.tmatesoft.svn.core.wc.SVNRevision; +import org.tmatesoft.svn.core.wc.SVNStatus; +import org.tmatesoft.svn.core.wc.SVNStatusClient; +import org.tmatesoft.svn.core.wc.SVNStatusType; +import org.tmatesoft.svn.core.wc.SVNUpdateClient; +import org.tmatesoft.svn.core.wc.SVNWCUtil; +import org.tmatesoft.svn.core.wc2.SvnCheckout; +import org.tmatesoft.svn.core.wc2.SvnTarget; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@RunWith(Parameterized.class) +public class SvnBlameCommandIT { + + /* + * Note about SONARSCSVN-11: The case of a project baseDir is in a subFolder of working copy is part of method tests by default + */ + + private static final String DUMMY_JAVA = "src/main/java/org/dummy/Dummy.java"; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Rule + public LogTester logTester = new LogTester(); + + private FileSystem fs; + private BlameInput input; + private String serverVersion; + private int wcVersion; + + @Parameters(name = "SVN server version {0}, WC version {1}") + public static Iterable<Object[]> data() { + return Arrays.asList(new Object[][] {{"1.6", 10}, {"1.7", 29}, {"1.8", 31}, {"1.9", 31}}); + } + + public SvnBlameCommandIT(String serverVersion, int wcVersion) { + this.serverVersion = serverVersion; + this.wcVersion = wcVersion; + } + + @Before + public void prepare() { + fs = mock(FileSystem.class); + input = mock(BlameInput.class); + when(input.fileSystem()).thenReturn(fs); + } + + @Test + public void testParsingOfOutput() throws Exception { + File repoDir = unzip("repo-svn.zip"); + + String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); + File baseDir = new File(checkout(scmUrl), "dummy-svn"); + + when(fs.baseDir()).thenReturn(baseDir); + DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) + .setLines(27) + .setModuleBaseDir(baseDir.toPath()) + .build(); + + BlameOutput blameResult = mock(BlameOutput.class); + when(input.filesToBlame()).thenReturn(singletonList(inputFile)); + + newSvnBlameCommand().blame(input, blameResult); + ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class); + verify(blameResult).blameResult(eq(inputFile), captor.capture()); + List<BlameLine> result = captor.getValue(); + assertThat(result).hasSize(27); + Date commitDate = new Date(1342691097393L); + BlameLine[] expected = IntStream.rangeClosed(1, 27).mapToObj(i -> new BlameLine().date(commitDate).revision("2").author("dgageot")).toArray(BlameLine[]::new); + assertThat(result).containsExactly(expected); + } + + private File unzip(String repoName) throws IOException { + File repoDir = temp.newFolder(); + try { + javaUnzip(Paths.get(this.getClass().getResource("test-repos").toURI()).resolve(serverVersion).resolve(repoName).toFile(), repoDir); + return repoDir; + } catch (URISyntaxException e) { + throw new IOException(e); + } + } + + private File checkout(String scmUrl) throws Exception { + ISVNOptions options = SVNWCUtil.createDefaultOptions(true); + ISVNAuthenticationManager isvnAuthenticationManager = SVNWCUtil.createDefaultAuthenticationManager(null, null, (char[]) null, false); + SVNClientManager svnClientManager = SVNClientManager.newInstance(options, isvnAuthenticationManager); + File out = temp.newFolder(); + SVNUpdateClient updateClient = svnClientManager.getUpdateClient(); + SvnCheckout co = updateClient.getOperationsFactory().createCheckout(); + co.setUpdateLocksOnDemand(updateClient.isUpdateLocksOnDemand()); + co.setSource(SvnTarget.fromURL(SVNURL.parseURIEncoded(scmUrl), SVNRevision.HEAD)); + co.setSingleTarget(SvnTarget.fromFile(out)); + co.setRevision(SVNRevision.HEAD); + co.setDepth(SVNDepth.INFINITY); + co.setAllowUnversionedObstructions(false); + co.setIgnoreExternals(updateClient.isIgnoreExternals()); + co.setExternalsHandler(SvnCodec.externalsHandler(updateClient.getExternalsHandler())); + co.setTargetWorkingCopyFormat(wcVersion); + co.run(); + return out; + } + + @Test + public void testParsingOfOutputWithMergeHistory() throws Exception { + File repoDir = unzip("repo-svn-with-merge.zip"); + + String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); + File baseDir = new File(checkout(scmUrl), "dummy-svn/trunk"); + + when(fs.baseDir()).thenReturn(baseDir); + DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) + .setLines(27) + .setModuleBaseDir(baseDir.toPath()) + .build(); + + BlameOutput blameResult = mock(BlameOutput.class); + when(input.filesToBlame()).thenReturn(singletonList(inputFile)); + + newSvnBlameCommand().blame(input, blameResult); + ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class); + verify(blameResult).blameResult(eq(inputFile), captor.capture()); + List<BlameLine> result = captor.getValue(); + assertThat(result).hasSize(27); + Date commitDate = new Date(1342691097393L); + Date revision6Date = new Date(1415262184300L); + + BlameLine[] expected = IntStream.rangeClosed(1, 27).mapToObj(i -> { + if (i == 2 || i == 24) { + return new BlameLine().date(revision6Date).revision("6").author("henryju"); + } else { + return new BlameLine().date(commitDate).revision("2").author("dgageot"); + } + }).toArray(BlameLine[]::new); + + assertThat(result).containsExactly(expected); + } + + @Test + public void shouldNotFailIfFileContainsLocalModification() throws Exception { + File repoDir = unzip("repo-svn.zip"); + + String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); + File baseDir = new File(checkout(scmUrl), "dummy-svn"); + + when(fs.baseDir()).thenReturn(baseDir); + DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA) + .setLines(28) + .setModuleBaseDir(baseDir.toPath()) + .build(); + + Files.write(baseDir.toPath().resolve(DUMMY_JAVA), "\n//foo".getBytes(), StandardOpenOption.APPEND); + + BlameOutput blameResult = mock(BlameOutput.class); + when(input.filesToBlame()).thenReturn(singletonList(inputFile)); + + newSvnBlameCommand().blame(input, blameResult); + verifyNoInteractions(blameResult); + } + + // SONARSCSVN-7 + @Test + public void shouldNotFailOnWrongFilename() throws Exception { + File repoDir = unzip("repo-svn.zip"); + + String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); + File baseDir = new File(checkout(scmUrl), "dummy-svn"); + + when(fs.baseDir()).thenReturn(baseDir); + DefaultInputFile inputFile = new TestInputFileBuilder("foo", DUMMY_JAVA.toLowerCase()) + .setLines(27) + .setModuleBaseDir(baseDir.toPath()) + .build(); + + BlameOutput blameResult = mock(BlameOutput.class); + when(input.filesToBlame()).thenReturn(singletonList(inputFile)); + + newSvnBlameCommand().blame(input, blameResult); + verifyNoInteractions(blameResult); + } + + @Test + public void shouldNotFailOnUncommitedFile() throws Exception { + File repoDir = unzip("repo-svn.zip"); + + String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); + File baseDir = new File(checkout(scmUrl), "dummy-svn"); + + when(fs.baseDir()).thenReturn(baseDir); + String relativePath = "src/main/java/org/dummy/Dummy2.java"; + DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath) + .setLines(28) + .setModuleBaseDir(baseDir.toPath()) + .build(); + + Files.write(baseDir.toPath().resolve(relativePath), "package org.dummy;\npublic class Dummy2 {}".getBytes()); + + BlameOutput blameResult = mock(BlameOutput.class); + when(input.filesToBlame()).thenReturn(singletonList(inputFile)); + + newSvnBlameCommand().blame(input, blameResult); + verifyNoInteractions(blameResult); + } + + @Test + public void shouldNotFailOnUncommitedDir() throws Exception { + File repoDir = unzip("repo-svn.zip"); + + String scmUrl = "file:///" + unixPath(new File(repoDir, "repo-svn")); + File baseDir = new File(checkout(scmUrl), "dummy-svn"); + + when(fs.baseDir()).thenReturn(baseDir); + String relativePath = "src/main/java/org/dummy2/dummy/Dummy.java"; + DefaultInputFile inputFile = new TestInputFileBuilder("foo", relativePath) + .setLines(28) + .setModuleBaseDir(baseDir.toPath()) + .build(); + + Path filepath = new File(baseDir, relativePath).toPath(); + Files.createDirectories(filepath.getParent()); + Files.write(filepath, "package org.dummy;\npublic class Dummy {}".getBytes()); + + BlameOutput blameResult = mock(BlameOutput.class); + when(input.filesToBlame()).thenReturn(singletonList(inputFile)); + + newSvnBlameCommand().blame(input, blameResult); + verifyNoInteractions(blameResult); + } + + @Test + public void blame_givenNoCredentials_logWarning() throws Exception { + BlameOutput output = mock(BlameOutput.class); + InputFile inputFile = mock(InputFile.class); + SvnBlameCommand svnBlameCommand = newSvnBlameCommand(); + + SVNClientManager clientManager = mock(SVNClientManager.class); + SVNLogClient logClient = mock(SVNLogClient.class); + SVNStatusClient statusClient = mock(SVNStatusClient.class); + SVNStatus status = mock(SVNStatus.class); + + when(clientManager.getLogClient()).thenReturn(logClient); + when(clientManager.getStatusClient()).thenReturn(statusClient); + when(status.getContentsStatus()).thenReturn(SVNStatusType.STATUS_NORMAL); + when(inputFile.file()).thenReturn(mock(File.class)); + when(statusClient.doStatus(any(File.class), anyBoolean())).thenReturn(status); + + doThrow(SVNAuthenticationException.class).when(logClient).doAnnotate(any(File.class), any(SVNRevision.class), + any(SVNRevision.class), any(SVNRevision.class), anyBoolean(), anyBoolean(), any(AnnotationHandler.class), + eq(null)); + + assertThrows(IllegalStateException.class, () -> { + svnBlameCommand.blame(clientManager, inputFile, output); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Authentication to SVN server is required but no " + + "authentication data was passed to the scanner"); + }); + + } + + @Test + public void blame_givenCredentialsSupplied_doNotlogWarning() throws Exception { + BlameOutput output = mock(BlameOutput.class); + InputFile inputFile = mock(InputFile.class); + SvnConfiguration properties = mock(SvnConfiguration.class); + SvnBlameCommand svnBlameCommand = new SvnBlameCommand(properties); + + SVNClientManager clientManager = mock(SVNClientManager.class); + SVNLogClient logClient = mock(SVNLogClient.class); + SVNStatusClient statusClient = mock(SVNStatusClient.class); + SVNStatus status = mock(SVNStatus.class); + + when(properties.isEmpty()).thenReturn(true); + when(clientManager.getLogClient()).thenReturn(logClient); + when(clientManager.getStatusClient()).thenReturn(statusClient); + when(status.getContentsStatus()).thenReturn(SVNStatusType.STATUS_NORMAL); + when(inputFile.file()).thenReturn(mock(File.class)); + when(statusClient.doStatus(any(File.class), anyBoolean())).thenReturn(status); + + doThrow(SVNAuthenticationException.class).when(logClient).doAnnotate(any(File.class), any(SVNRevision.class), + any(SVNRevision.class), any(SVNRevision.class), anyBoolean(), anyBoolean(), any(AnnotationHandler.class), + eq(null)); + + assertThrows(IllegalStateException.class, () -> { + svnBlameCommand.blame(clientManager, inputFile, output); + assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); + }); + + } + + private static void javaUnzip(File zip, File toDir) { + try { + try (ZipFile zipFile = new ZipFile(zip)) { + Enumeration<? extends ZipEntry> entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File to = new File(toDir, entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(to.toPath()); + } else { + File parent = to.getParentFile(); + if (parent != null) { + Files.createDirectories(parent.toPath()); + } + + Files.copy(zipFile.getInputStream(entry), to.toPath()); + } + } + } + } catch (Exception e) { + throw new IllegalStateException("Fail to unzip " + zip + " to " + toDir, e); + } + } + + private static String unixPath(File file) { + return file.getAbsolutePath().replace('\\', '/'); + } + + private SvnBlameCommand newSvnBlameCommand() { + return new SvnBlameCommand(mock(SvnConfiguration.class)); + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnTester.java b/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnTester.java new file mode 100644 index 00000000000..c24f592c064 --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnTester.java @@ -0,0 +1,129 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scm.svn; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.tmatesoft.svn.core.SVNDepth; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.io.SVNRepositoryFactory; +import org.tmatesoft.svn.core.wc.SVNClientManager; +import org.tmatesoft.svn.core.wc.SVNCopyClient; +import org.tmatesoft.svn.core.wc.SVNCopySource; +import org.tmatesoft.svn.core.wc.SVNRevision; +import org.tmatesoft.svn.core.wc.SVNUpdateClient; +import org.tmatesoft.svn.core.wc2.SvnList; +import org.tmatesoft.svn.core.wc2.SvnOperationFactory; +import org.tmatesoft.svn.core.wc2.SvnRemoteMkDir; +import org.tmatesoft.svn.core.wc2.SvnTarget; + +public class SvnTester { + private final SVNClientManager manager = SVNClientManager.newInstance(new SvnOperationFactory()); + + private final SVNURL localRepository; + + public SvnTester(Path root) throws SVNException, IOException { + localRepository = SVNRepositoryFactory.createLocalRepository(root.toFile(), false, false); + mkdir("trunk"); + mkdir("branches"); + } + + private void mkdir(String relpath) throws IOException, SVNException { + SvnRemoteMkDir remoteMkDir = manager.getOperationFactory().createRemoteMkDir(); + remoteMkDir.addTarget(SvnTarget.fromURL(localRepository.appendPath(relpath, false))); + remoteMkDir.run(); + } + + public void createBranch(String branchName) throws IOException, SVNException { + SVNCopyClient copyClient = manager.getCopyClient(); + SVNCopySource source = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, localRepository.appendPath("trunk", false)); + copyClient.doCopy(new SVNCopySource[] {source}, localRepository.appendPath("branches/" + branchName, false), false, false, true, "Create branch", null); + } + + public void checkout(Path worktree, String path) throws SVNException { + SVNUpdateClient updateClient = manager.getUpdateClient(); + updateClient.doCheckout(localRepository.appendPath(path, false), + worktree.toFile(), null, null, SVNDepth.INFINITY, false); + } + + public void add(Path worktree, String filename) throws SVNException { + manager.getWCClient().doAdd(worktree.resolve(filename).toFile(), true, false, false, SVNDepth.INFINITY, false, false, true); + } + + public void copy(Path worktree, String src, String dst) throws SVNException { + SVNCopyClient copyClient = manager.getCopyClient(); + SVNCopySource source = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, worktree.resolve(src).toFile()); + copyClient.doCopy(new SVNCopySource[]{source}, worktree.resolve(dst).toFile(), false, false, true); + } + + public void commit(Path worktree) throws SVNException { + manager.getCommitClient().doCommit(new File[] {worktree.toFile()}, false, "commit " + worktree, null, null, false, false, SVNDepth.INFINITY); + } + + public void update(Path worktree) throws SVNException { + manager.getUpdateClient().doUpdate(new File[] {worktree.toFile()}, SVNRevision.HEAD, SVNDepth.INFINITY, false, false); + } + + public Collection<String> list(String... paths) throws SVNException { + Set<String> results = new HashSet<>(); + + SvnList list = manager.getOperationFactory().createList(); + if (paths.length == 0) { + list.addTarget(SvnTarget.fromURL(localRepository)); + } else { + for (String path : paths) { + list.addTarget(SvnTarget.fromURL(localRepository.appendPath(path, false))); + } + } + list.setDepth(SVNDepth.INFINITY); + list.setReceiver((svnTarget, svnDirEntry) -> { + String path = svnDirEntry.getRelativePath(); + if (!path.isEmpty()) { + results.add(path); + } + }); + list.run(); + + return results; + } + + public void createFile(Path worktree, String filename, String content) throws IOException { + Files.write(worktree.resolve(filename), content.getBytes()); + } + + public void createFile(Path worktree, String filename) throws IOException { + createFile(worktree, filename, filename + "\n"); + } + + public void appendToFile(Path worktree, String filename) throws IOException { + Files.write(worktree.resolve(filename), (filename + "\n").getBytes(), StandardOpenOption.APPEND); + } + + public void deleteFile(Path worktree, String filename) throws SVNException { + manager.getWCClient().doDelete(worktree.resolve(filename).toFile(), false, false); + } +} diff --git a/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnTesterIT.java b/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnTesterIT.java new file mode 100644 index 00000000000..a285a63cace --- /dev/null +++ b/sonar-scanner-engine/src/it/java/org/sonar/scm/svn/SvnTesterIT.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.scm.svn; + +import java.io.IOException; +import java.nio.file.Path; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tmatesoft.svn.core.SVNException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SvnTesterIT { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private SvnTester tester; + + @Before + public void before() throws IOException, SVNException { + tester = new SvnTester(temp.newFolder().toPath()); + } + + @Test + public void test_init() throws SVNException { + assertThat(tester.list()).containsExactlyInAnyOrder("trunk", "branches"); + } + + @Test + public void test_add_and_commit() throws IOException, SVNException { + assertThat(tester.list("trunk")).isEmpty(); + + Path worktree = temp.newFolder().toPath(); + tester.checkout(worktree, "trunk"); + tester.createFile(worktree, "file1"); + + tester.add(worktree, "file1"); + tester.commit(worktree); + + assertThat(tester.list("trunk")).containsOnly("file1"); + } + + @Test + public void test_createBranch() throws IOException, SVNException { + tester.createBranch("b1"); + assertThat(tester.list()).containsExactlyInAnyOrder("trunk", "branches", "branches/b1"); + assertThat(tester.list("branches")).containsOnly("b1"); + } +} diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.6/repo-svn-with-merge.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.6/repo-svn-with-merge.zip Binary files differnew file mode 100644 index 00000000000..d3c3ee5fa1e --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.6/repo-svn-with-merge.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.6/repo-svn.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.6/repo-svn.zip Binary files differnew file mode 100644 index 00000000000..291d4fb86f7 --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.6/repo-svn.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.7/repo-svn-with-merge.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.7/repo-svn-with-merge.zip Binary files differnew file mode 100644 index 00000000000..d3c3ee5fa1e --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.7/repo-svn-with-merge.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.7/repo-svn.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.7/repo-svn.zip Binary files differnew file mode 100644 index 00000000000..291d4fb86f7 --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.7/repo-svn.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.8/repo-svn-with-merge.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.8/repo-svn-with-merge.zip Binary files differnew file mode 100644 index 00000000000..852a05e4574 --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.8/repo-svn-with-merge.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.8/repo-svn.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.8/repo-svn.zip Binary files differnew file mode 100644 index 00000000000..f31352c5e03 --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.8/repo-svn.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.9/repo-svn-with-merge.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.9/repo-svn-with-merge.zip Binary files differnew file mode 100644 index 00000000000..ca672fd6681 --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.9/repo-svn-with-merge.zip diff --git a/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.9/repo-svn.zip b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.9/repo-svn.zip Binary files differnew file mode 100644 index 00000000000..39b241cd698 --- /dev/null +++ b/sonar-scanner-engine/src/it/resources/org/sonar/scm/svn/test-repos/1.9/repo-svn.zip |