diff --git a/java-checks-testkit/A.java b/java-checks-testkit/A.java new file mode 100644 index 00000000000..01e42ca697d --- /dev/null +++ b/java-checks-testkit/A.java @@ -0,0 +1,3 @@ +public class A { + private void BAD_METHOD_NAME() {} +} diff --git a/java-checks-testkit/pom.xml b/java-checks-testkit/pom.xml index 3da9d7a1910..41e8f15374b 100644 --- a/java-checks-testkit/pom.xml +++ b/java-checks-testkit/pom.xml @@ -60,6 +60,11 @@ sonar-analyzer-test-commons compile + + com.sonarsource.scanner.integrationtester + sonar-scanner-integration-tester + ${sonar.integration.tester.version} + diff --git a/java-checks-testkit/src/A.java b/java-checks-testkit/src/A.java new file mode 100644 index 00000000000..01e42ca697d --- /dev/null +++ b/java-checks-testkit/src/A.java @@ -0,0 +1,3 @@ +public class A { + private void BAD_METHOD_NAME() {} +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/JavaRuleMetadata.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/JavaRuleMetadata.java new file mode 100644 index 00000000000..86172be43fe --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/JavaRuleMetadata.java @@ -0,0 +1,99 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.sonarsource.scanner.integrationtester.dsl.ActiveRule; +import com.sonarsource.scanner.integrationtester.dsl.RuleKey; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Resolves Java rule metadata from the sonar-java-plugin resource files + * and builds {@link ActiveRule} instances from a single rule key string. + * + *

Usage: + *

{@code
+ * ActiveRule rule = JavaRuleMetadata.activeRule("S100");
+ * }
+ */ +public final class JavaRuleMetadata { + + private static final String RULES_METADATA_DIR = "sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java"; + private static final String REPO_KEY = "java"; + private static final String LANGUAGE_KEY = "java"; + + private static final Path METADATA_ROOT = resolveMetadataRoot(); + private static final Map CACHE = new ConcurrentHashMap<>(); + + private JavaRuleMetadata() { + } + + /** + * Builds an {@link ActiveRule} for the given rule key (e.g. {@code "S100"}) + * by reading title and defaultSeverity from the rule's JSON metadata file. + */ + public static ActiveRule activeRule(String ruleKey) { + return CACHE.computeIfAbsent(ruleKey, JavaRuleMetadata::loadRule); + } + + private static ActiveRule loadRule(String ruleKey) { + Path jsonFile = METADATA_ROOT.resolve(ruleKey + ".json"); + if (!Files.exists(jsonFile)) { + throw new IllegalArgumentException("Rule metadata not found for key '" + ruleKey + "': " + jsonFile); + } + + JsonObject json = readJson(jsonFile); + String title = json.get("title").getAsString(); + String severityStr = json.get("defaultSeverity").getAsString(); + ActiveRule.Severity severity = ActiveRule.Severity.valueOf(severityStr.toUpperCase(Locale.ROOT)); + + return ActiveRule.builder() + .withKey(RuleKey.of(REPO_KEY, ruleKey)) + .withName(title) + .withLanguageKey(LANGUAGE_KEY) + .withSeverity(severity) + .build(); + } + + private static JsonObject readJson(Path path) { + try { + String content = Files.readString(path); + return JsonParser.parseString(content).getAsJsonObject(); + } catch (IOException e) { + throw new IllegalStateException("Failed to read rule metadata: " + path, e); + } + } + + private static Path resolveMetadataRoot() { + Path lookUpPath = Path.of(System.getProperty("user.dir")); + while (lookUpPath != null) { + Path candidate = lookUpPath.resolve(RULES_METADATA_DIR); + if (Files.isDirectory(candidate)) { + return candidate; + } + lookUpPath = lookUpPath.getParent(); + } + throw new IllegalStateException( + "Cannot find rule metadata directory '" + RULES_METADATA_DIR + "' from working directory"); + } +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ModuleBuilder.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ModuleBuilder.java new file mode 100644 index 00000000000..15c5942505d --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ModuleBuilder.java @@ -0,0 +1,97 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ModuleBuilder { + + private final String name; + private final ScannerVerifierProjectBuilder projectBuilder; + private final List inputFiles = new ArrayList<>(); + private final List binaries = new ArrayList<>(); + private final List libraries = new ArrayList<>(); + private final List dependencies = new ArrayList<>(); + + ModuleBuilder(String name, ScannerVerifierProjectBuilder projectBuilder) { + this.name = name; + this.projectBuilder = projectBuilder; + } + + /** + * Source files to analyze, mapped to {@code sonar.sources}. + */ + public ModuleBuilder withInputFiles(Path... files) { + Collections.addAll(inputFiles, files); + return this; + } + + /** + * Compiled class directories, mapped to {@code sonar.java.binaries}. + */ + public ModuleBuilder withBinaries(Path... paths) { + Collections.addAll(binaries, paths); + return this; + } + + /** + * External library jars, mapped to {@code sonar.java.libraries}. + */ + public ModuleBuilder withLibraries(Path... libs) { + Collections.addAll(libraries, libs); + return this; + } + + /** + * Inter-module dependencies by module name. The dependent modules' binaries + * are automatically added to this module's {@code sonar.java.libraries}. + */ + public ModuleBuilder withDependencies(String... moduleNames) { + Collections.addAll(dependencies, moduleNames); + return this; + } + + /** + * Returns to the project builder to add more modules or finalize. + */ + public ScannerVerifierProjectBuilder endModule() { + return projectBuilder; + } + + String name() { + return name; + } + + List inputFiles() { + return inputFiles; + } + + List binaries() { + return binaries; + } + + List libraries() { + return libraries; + } + + List dependencies() { + return dependencies; + } +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifier.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifier.java new file mode 100644 index 00000000000..8d55bca0149 --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifier.java @@ -0,0 +1,39 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.runner.ScannerRunner; +import com.sonarsource.scanner.integrationtester.runner.ScannerRunnerConfig; + +public class ScannerVerifier { + + private static final ScannerRunnerConfig RUNNER_CONFIG = ScannerRunnerConfig.builder() + .withLogsPrintedToStdOut(true) + .build(); + + private ScannerVerifier() { + // Utility class, do not instantiate + } + + public static ScannerVerifierResult execute(ScannerVerifierAnalysisBuilder analysis) { + var sonarServerContext = analysis.buildServerContext(); + var scannerInput = analysis.buildScannerInput(); + var result = ScannerRunner.run(sonarServerContext, scannerInput, RUNNER_CONFIG); + return new ScannerVerifierResult(result); + } + +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierAnalysisBuilder.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierAnalysisBuilder.java new file mode 100644 index 00000000000..4c27c5ccb63 --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierAnalysisBuilder.java @@ -0,0 +1,105 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.EngineVersion; +import com.sonarsource.scanner.integrationtester.dsl.ScannerInput; +import com.sonarsource.scanner.integrationtester.dsl.SonarServerContext; +import org.sonar.java.annotations.VisibleForTesting; +import org.sonar.java.test.classpath.TestClasspathUtils; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ScannerVerifierAnalysisBuilder { + + private static final Path JAVA_PLUGIN_PATH = TestClasspathUtils.findModuleJarPath("../sonar-java-plugin"); + private static final String DEFAULT_PROJECT_KEY = "scanner-verifier-test"; + + private final SonarServerContext.Builder serverContextBuilder; + private final Map projectScannerProperties = new LinkedHashMap<>(); + private final List modules = new ArrayList<>(); + private Path projectBaseDir; + private String projectKey = DEFAULT_PROJECT_KEY; + + private ScannerVerifierAnalysisBuilder() { + serverContextBuilder = SonarServerContext.builder() + .withProduct(SonarServerContext.Product.SERVER) + .withEngineVersion(EngineVersion.latestRelease()) + .withLanguage("java", "Java", ".java") + .withPlugin(JAVA_PLUGIN_PATH); + } + + public static ScannerVerifierAnalysisBuilder newAnalysis(Path projectBaseDir) { + return new ScannerVerifierAnalysisBuilder() + .withProjectBaseDir(projectBaseDir) + .withProjectKey("project"); + } + + public ScannerVerifierAnalysisBuilder withProject(ScannerVerifierProjectBuilder project) { + serverContextBuilder.withProjectContext(project.build()); + projectScannerProperties.putAll(project.toScannerProperties()); + modules.addAll(project.modules()); + return this; + } + + private ScannerVerifierAnalysisBuilder withProjectBaseDir(Path projectBaseDir) { + this.projectBaseDir = projectBaseDir; + return this; + } + + private ScannerVerifierAnalysisBuilder withProjectKey(String projectKey) { + this.projectKey = projectKey; + return this; + } + + protected SonarServerContext buildServerContext() { + createModuleDirectories(); + return serverContextBuilder.build(); + } + + @VisibleForTesting + protected void createModuleDirectories() { + for (ModuleBuilder module : modules) { + Path moduleDir = projectBaseDir.resolve(module.name()); + Path srcDir = moduleDir.resolve("src"); + try { + Files.createDirectories(srcDir); + for (Path inputFile : module.inputFiles()) { + Files.copy(inputFile, srcDir.resolve(inputFile.getFileName()), StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to set up module directory: " + moduleDir, e); + } + } + } + + protected ScannerInput buildScannerInput() { + var builder = ScannerInput.create(projectKey, projectBaseDir); + if (!projectScannerProperties.isEmpty()) { + builder.withScannerProperties(projectScannerProperties); + } + return builder.build(); + } +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierProjectBuilder.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierProjectBuilder.java new file mode 100644 index 00000000000..391e8833795 --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierProjectBuilder.java @@ -0,0 +1,162 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.ActiveRule; +import com.sonarsource.scanner.integrationtester.dsl.SonarProjectContext; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * Builder to construct a {@link SonarProjectContext} and module-level scanner properties + * for multi-module project analysis. + * + *

Usage example: + *

{@code
+ * var project = ScannerVerifierProjectBuilder.create()
+ *   .withActiveRules(activeRules)
+ *   .addModule("moduleA")
+ *     .withInputFiles(Path.of("src/A.java"))
+ *     .withBinaries(Path.of("target/classes"))
+ *     .endModule()
+ *   .addModule("moduleB")
+ *     .withInputFiles(Path.of("src/B.java"))
+ *     .withBinaries(Path.of("target/classes"))
+ *     .withDependencies("moduleA")
+ *     .endModule();
+ *
+ * SonarProjectContext context = project.build();
+ * Map properties = project.toScannerProperties();
+ * }
+ * + *

When a module declares a dependency on another module via {@link ModuleBuilder#withDependencies(String...)}, + * the dependent module's binaries are automatically added to the depending module's {@code sonar.java.libraries}. + */ +public class ScannerVerifierProjectBuilder { + + private final List activeRules = new ArrayList<>(); + private final Map modules = new LinkedHashMap<>(); + + private ScannerVerifierProjectBuilder() { + } + + public static ScannerVerifierProjectBuilder newProject() { + return new ScannerVerifierProjectBuilder(); + } + + public ScannerVerifierProjectBuilder withActiveRules(List rules) { + activeRules.addAll(rules); + return this; + } + + /** + * Adds an active rule by key (e.g. {@code "S100"}), automatically resolving + * name and severity from the sonar-java-plugin rule metadata. + */ + public ScannerVerifierProjectBuilder withActiveRule(String ruleKey) { + activeRules.add(JavaRuleMetadata.activeRule(ruleKey)); + return this; + } + + public ModuleBuilder addModule(String name) { + Objects.requireNonNull(name, "Module name cannot be null"); + if (modules.containsKey(name)) { + throw new IllegalArgumentException("Module '" + name + "' already exists"); + } + var module = new ModuleBuilder(name, this); + modules.put(name, module); + return module; + } + + /** + * Builds a {@link SonarProjectContext} with the configured active rules. + */ + public SonarProjectContext build() { + validateDependencies(); + return SonarProjectContext.builder() + .withActiveRules(activeRules) + .build(); + } + + /** + * Generates scanner properties for multi-module project configuration. + *

+ * Produces properties such as {@code sonar.modules}, and for each module: + * {@code .sonar.sources}, {@code .sonar.java.binaries}, + * and {@code .sonar.java.libraries} (including resolved inter-module dependencies). + */ + public Map toScannerProperties() { + validateDependencies(); + Map properties = new LinkedHashMap<>(); + if (modules.isEmpty()) { + return properties; + } + + properties.put("sonar.modules", String.join(",", modules.keySet())); + + for (ModuleBuilder module : modules.values()) { + String prefix = module.name() + "."; + + if (!module.inputFiles().isEmpty()) { + properties.put(prefix + "sonar.sources", "src"); + } + + if (!module.binaries().isEmpty()) { + properties.put(prefix + "sonar.java.binaries", + module.binaries().stream().map(Path::toString).collect(Collectors.joining(","))); + } + + List allLibraries = new ArrayList<>(); + module.libraries().stream().map(Path::toString).forEach(allLibraries::add); + + // Resolve inter-module dependencies: dependent module binaries become libraries + for (String dep : module.dependencies()) { + ModuleBuilder depModule = modules.get(dep); + depModule.binaries().stream().map(Path::toString).forEach(allLibraries::add); + } + + if (!allLibraries.isEmpty()) { + properties.put(prefix + "sonar.java.libraries", + String.join(",", allLibraries)); + } + } + + return properties; + } + + Collection modules() { + return modules.values(); + } + + private void validateDependencies() { + for (ModuleBuilder module : modules.values()) { + for (String dep : module.dependencies()) { + if (!modules.containsKey(dep)) { + throw new IllegalStateException( + "Module '" + module.name() + "' depends on unknown module '" + dep + "'"); + } + } + } + } +} diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierResult.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierResult.java new file mode 100644 index 00000000000..4865c801c29 --- /dev/null +++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/sit/ScannerVerifierResult.java @@ -0,0 +1,41 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.ScannerResult; +import com.sonarsource.scanner.integrationtester.dsl.ScannerResultSuccess; +import com.sonarsource.scanner.integrationtester.dsl.issue.FileIssue; + +import java.util.List; + +public class ScannerVerifierResult { + + private final ScannerResult scannerResult; + + protected ScannerVerifierResult(ScannerResult scannerResult) { + this.scannerResult = scannerResult; + assert scannerResult.exitCode() != 1 : "Scanner execution failed with exit code 1, expected 0. Check the logs for more details."; + } + + public List getAllIssues() { + ScannerResultSuccess successResult = (ScannerResultSuccess) scannerResult; + return successResult.scannerOutputReader().getFiles().stream() + .flatMap(file -> file.getIssues().stream()) + .toList(); + } + +} diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/JavaRuleMetadataTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/JavaRuleMetadataTest.java new file mode 100644 index 00000000000..1c3c73ecb28 --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/JavaRuleMetadataTest.java @@ -0,0 +1,52 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.ActiveRule; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class JavaRuleMetadataTest { + + @Test + void activeRule_loads_title_and_severity() { + ActiveRule rule = JavaRuleMetadata.activeRule("S100"); + + assertThat(rule.ruleKey().repository()).isEqualTo("java"); + assertThat(rule.ruleKey().rule()).isEqualTo("S100"); + assertThat(rule.ruleName()).isEqualTo("Method names should comply with a naming convention"); + assertThat(rule.severity()).isEqualTo(ActiveRule.Severity.MINOR); + assertThat(rule.languageKey()).isEqualTo("java"); + } + + @Test + void activeRule_caches_results() { + ActiveRule first = JavaRuleMetadata.activeRule("S101"); + ActiveRule second = JavaRuleMetadata.activeRule("S101"); + + assertThat(first).isSameAs(second); + } + + @Test + void activeRule_throws_for_unknown_rule() { + assertThatIllegalArgumentException() + .isThrownBy(() -> JavaRuleMetadata.activeRule("S999999")) + .withMessageContaining("S999999"); + } +} diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ModuleBuilderTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ModuleBuilderTest.java new file mode 100644 index 00000000000..884da98ae5e --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ModuleBuilderTest.java @@ -0,0 +1,118 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ModuleBuilderTest { + + @Test + void endModule_returns_project_builder() { + ScannerVerifierProjectBuilder projectBuilder = ScannerVerifierProjectBuilder.newProject(); + ModuleBuilder moduleBuilder = projectBuilder.addModule("mod"); + + assertThat(moduleBuilder.endModule()).isSameAs(projectBuilder); + } + + @Test + void withInputFiles_adds_files() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("mod") + .withInputFiles(Path.of("src/A.java"), Path.of("src/B.java")) + .endModule() + .toScannerProperties(); + + assertThat(props).containsEntry("mod.sonar.sources", "src"); + } + + @Test + void withBinaries_adds_binaries() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("mod") + .withBinaries(Path.of("target/classes"), Path.of("target/extra")) + .endModule() + .toScannerProperties(); + + assertThat(props).containsEntry("mod.sonar.java.binaries", "target/classes,target/extra"); + } + + @Test + void withLibraries_adds_libraries() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("mod") + .withLibraries(Path.of("lib/a.jar"), Path.of("lib/b.jar")) + .endModule() + .toScannerProperties(); + + assertThat(props).containsEntry("mod.sonar.java.libraries", "lib/a.jar,lib/b.jar"); + } + + @Test + void withDependencies_resolves_dependent_binaries_as_libraries() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("core") + .withInputFiles(Path.of("src/Core.java")) + .withBinaries(Path.of("core/target/classes")) + .endModule() + .addModule("app") + .withInputFiles(Path.of("src/App.java")) + .withDependencies("core") + .endModule() + .toScannerProperties(); + + assertThat(props).containsEntry("app.sonar.java.libraries", "core/target/classes"); + } + + @Test + void withDependencies_merges_with_explicit_libraries() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("core") + .withBinaries(Path.of("core/target/classes")) + .endModule() + .addModule("app") + .withLibraries(Path.of("lib/ext.jar")) + .withDependencies("core") + .endModule() + .toScannerProperties(); + + assertThat(props.get("app.sonar.java.libraries")) + .contains("lib/ext.jar") + .contains("core/target/classes"); + } + + @Test + void multiple_calls_accumulate() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("mod") + .withInputFiles(Path.of("src/A.java")) + .withInputFiles(Path.of("src/B.java")) + .withBinaries(Path.of("target/classes")) + .withBinaries(Path.of("target/extra")) + .withLibraries(Path.of("lib/a.jar")) + .withLibraries(Path.of("lib/b.jar")) + .endModule() + .toScannerProperties(); + + assertThat(props.get("mod.sonar.sources")).isEqualTo("src"); + assertThat(props.get("mod.sonar.java.binaries")).isEqualTo("target/classes,target/extra"); + assertThat(props.get("mod.sonar.java.libraries")).isEqualTo("lib/a.jar,lib/b.jar"); + } +} diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierAnalysisBuilderTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierAnalysisBuilderTest.java new file mode 100644 index 00000000000..66167c66607 --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierAnalysisBuilderTest.java @@ -0,0 +1,189 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.ActiveRule; +import com.sonarsource.scanner.integrationtester.dsl.RuleKey; +import com.sonarsource.scanner.integrationtester.dsl.ScannerInput; +import com.sonarsource.scanner.integrationtester.dsl.SonarServerContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ScannerVerifierAnalysisBuilderTest { + + private static final ActiveRule RULE = ActiveRule.builder() + .withKey(RuleKey.of("java", "S1234")) + .withName("Test Rule") + .withLanguageKey("java") + .withSeverity(ActiveRule.Severity.MAJOR) + .build(); + + @TempDir + Path tempDir; + + @TempDir + Path sourceDir; + + private Path createSourceFile(String name, String content) throws IOException { + Path file = sourceDir.resolve(name); + Files.writeString(file, content); + return file; + } + + @Test + void withProject_sets_project_context_on_server_context() throws Exception { + Path aJava = createSourceFile("A.java", "class A {}"); + var project = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE)) + .addModule("mod") + .withInputFiles(aJava) + .endModule(); + + SonarServerContext serverContext = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .withProject(project) + .buildServerContext(); + + assertThat(serverContext.projectContext()).isNotNull(); + assertThat(serverContext.projectContext().activeRules()).containsExactly(RULE); + } + + @Test + void buildScannerInput_includes_project_scanner_properties() throws Exception { + Path coreJava = createSourceFile("Core.java", "class Core {}"); + Path appJava = createSourceFile("App.java", "class App {}"); + var project = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE)) + .addModule("core") + .withInputFiles(coreJava) + .withBinaries(Path.of("target/classes")) + .endModule() + .addModule("app") + .withInputFiles(appJava) + .withDependencies("core") + .endModule(); + + ScannerInput input = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .withProject(project) + .buildScannerInput(); + + assertThat(input.scannerProperties()) + .containsEntry("sonar.modules", "core,app") + .containsEntry("core.sonar.sources", "src") + .containsEntry("core.sonar.java.binaries", "target/classes") + .containsEntry("app.sonar.sources", "src") + .containsEntry("app.sonar.java.libraries", "target/classes"); + } + + @Test + void buildScannerInput_uses_provided_base_dir() { + ScannerInput input = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .buildScannerInput(); + + assertThat(input.projectBaseDir()).isEqualTo(tempDir); + } + + @Test + void buildScannerInput_empty_properties_when_no_project() { + ScannerInput input = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .buildScannerInput(); + + assertThat(input.scannerProperties()).isEmpty(); + } + + @Test + void buildServerContext_returns_valid_context_without_project() { + SonarServerContext serverContext = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .buildServerContext(); + + assertThat(serverContext.product()).isEqualTo(SonarServerContext.Product.SERVER); + assertThat(serverContext.projectContext()).isNull(); + } + + @Test + void createModuleDirectories_creates_src_subdirectory_for_each_module() { + var project = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE)) + .addModule("moduleA") + .endModule() + .addModule("moduleB") + .endModule(); + + var analysis = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .withProject(project); + + analysis.createModuleDirectories(); + + assertThat(tempDir.resolve("moduleA/src")).isDirectory(); + assertThat(tempDir.resolve("moduleB/src")).isDirectory(); + } + + @Test + void createModuleDirectories_copies_input_files_into_src() throws Exception { + Path aJava = createSourceFile("A.java", "class A {}"); + Path bJava = createSourceFile("B.java", "class B {}"); + + var project = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE)) + .addModule("mod") + .withInputFiles(aJava, bJava) + .endModule(); + + var analysis = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .withProject(project); + + analysis.createModuleDirectories(); + + assertThat(tempDir.resolve("mod/src/A.java")).isRegularFile().hasContent("class A {}"); + assertThat(tempDir.resolve("mod/src/B.java")).isRegularFile().hasContent("class B {}"); + } + + @Test + void createModuleDirectories_is_idempotent() throws Exception { + Path aJava = createSourceFile("A.java", "class A {}"); + + var project = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE)) + .addModule("mod") + .withInputFiles(aJava) + .endModule(); + + var analysis = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .withProject(project); + + analysis.createModuleDirectories(); + analysis.createModuleDirectories(); + + assertThat(tempDir.resolve("mod/src")).isDirectory(); + assertThat(tempDir.resolve("mod/src/A.java")).isRegularFile(); + } + + @Test + void createModuleDirectories_no_op_when_no_modules() throws Exception { + var analysis = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir); + + analysis.createModuleDirectories(); + + assertThat(Files.list(tempDir)).isEmpty(); + } +} diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierProjectBuilderTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierProjectBuilderTest.java new file mode 100644 index 00000000000..b9a030f8397 --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierProjectBuilderTest.java @@ -0,0 +1,164 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.ActiveRule; +import com.sonarsource.scanner.integrationtester.dsl.RuleKey; +import com.sonarsource.scanner.integrationtester.dsl.SonarProjectContext; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +class ScannerVerifierProjectBuilderTest { + + private static final ActiveRule RULE_1 = ActiveRule.builder() + .withKey(RuleKey.of("java", "S1234")) + .withName("Rule 1") + .withLanguageKey("java") + .withSeverity(ActiveRule.Severity.MAJOR) + .build(); + + private static final ActiveRule RULE_2 = ActiveRule.builder() + .withKey(RuleKey.of("java", "S5678")) + .withName("Rule 2") + .withLanguageKey("java") + .withSeverity(ActiveRule.Severity.INFO) + .build(); + + @Test + void build_produces_project_context_with_active_rules() { + SonarProjectContext context = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE_1, RULE_2)) + .addModule("mod") + .withInputFiles(Path.of("src/A.java")) + .endModule() + .build(); + + assertThat(context.activeRules()).containsExactly(RULE_1, RULE_2); + } + + @Test + void toScannerProperties_empty_modules_returns_empty_map() { + Map props = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE_1)) + .toScannerProperties(); + + assertThat(props).isEmpty(); + } + + @Test + void toScannerProperties_single_module() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("moduleA") + .withInputFiles(Path.of("src/A.java")) + .withBinaries(Path.of("target/classes")) + .withLibraries(Path.of("lib/dep.jar")) + .endModule() + .toScannerProperties(); + + assertThat(props).containsExactlyInAnyOrderEntriesOf(Map.of( + "sonar.modules", "moduleA", + "moduleA.sonar.sources", "src", + "moduleA.sonar.java.binaries", "target/classes", + "moduleA.sonar.java.libraries", "lib/dep.jar")); + } + + @Test + void toScannerProperties_multi_module_with_dependencies() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("core") + .withInputFiles(Path.of("core/src/Core.java")) + .withBinaries(Path.of("core/target/classes")) + .endModule() + .addModule("api") + .withInputFiles(Path.of("api/src/Api.java")) + .withBinaries(Path.of("api/target/classes")) + .withDependencies("core") + .endModule() + .addModule("app") + .withInputFiles(Path.of("app/src/App.java")) + .withBinaries(Path.of("app/target/classes")) + .withDependencies("core", "api") + .endModule() + .toScannerProperties(); + + assertThat(props.get("sonar.modules")).isEqualTo("core,api,app"); + assertThat(props.get("core.sonar.java.libraries")).isNull(); + assertThat(props.get("api.sonar.java.libraries")).isEqualTo("core/target/classes"); + assertThat(props.get("app.sonar.java.libraries")).isEqualTo("core/target/classes,api/target/classes"); + } + + @Test + void toScannerProperties_omits_empty_properties() { + Map props = ScannerVerifierProjectBuilder.newProject() + .addModule("mod") + .endModule() + .toScannerProperties(); + + assertThat(props).containsOnlyKeys("sonar.modules"); + } + + @Test + void addModule_rejects_null_name() { + ScannerVerifierProjectBuilder builder = ScannerVerifierProjectBuilder.newProject(); + + assertThatNullPointerException() + .isThrownBy(() -> builder.addModule(null)) + .withMessage("Module name cannot be null"); + } + + @Test + void addModule_rejects_duplicate_name() { + ScannerVerifierProjectBuilder builder = ScannerVerifierProjectBuilder.newProject(); + builder.addModule("mod").endModule(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> builder.addModule("mod")) + .withMessage("Module 'mod' already exists"); + } + + @Test + void build_rejects_unknown_dependency() { + ScannerVerifierProjectBuilder builder = ScannerVerifierProjectBuilder.newProject() + .withActiveRules(List.of(RULE_1)) + .addModule("app") + .withDependencies("nonexistent") + .endModule(); + + assertThatIllegalStateException() + .isThrownBy(builder::build) + .withMessage("Module 'app' depends on unknown module 'nonexistent'"); + } + + @Test + void toScannerProperties_rejects_unknown_dependency() { + ScannerVerifierProjectBuilder builder = ScannerVerifierProjectBuilder.newProject() + .addModule("app") + .withDependencies("nonexistent") + .endModule(); + + assertThatIllegalStateException() + .isThrownBy(builder::toScannerProperties) + .withMessage("Module 'app' depends on unknown module 'nonexistent'"); + } +} diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierTest.java new file mode 100644 index 00000000000..8769332b7fe --- /dev/null +++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/sit/ScannerVerifierTest.java @@ -0,0 +1,61 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.verifier.sit; + +import com.sonarsource.scanner.integrationtester.dsl.issue.FileIssue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ScannerVerifierTest { + + @TempDir + Path tempDir; + + @Test + void shouldRunScannerEngineAnalysis() throws IOException { + Path sourceFilePath = Path.of("A.java"); + Files.writeString(sourceFilePath, """ + public class A { + private void BAD_METHOD_NAME() {} + } + """); + + var analysis = ScannerVerifierAnalysisBuilder.newAnalysis(tempDir) + .withProject( + ScannerVerifierProjectBuilder.newProject() + .addModule("mod") + .withInputFiles(sourceFilePath) +// .withBinaries(Path.of("target/classes")) +// .withLibraries(Path.of("libs/lib.jar")) + .endModule() + .withActiveRule("S100") + ); + + var result = ScannerVerifier.execute(analysis); + assertThat(result.getAllIssues()).hasSize(1); + FileIssue actual = result.getAllIssues().get(0); + assertEquals("java:S100", actual.ruleKey()); + } + +} diff --git a/pom.xml b/pom.xml index a1f63ba7e16..e53874b941b 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,7 @@ its/**,java-checks-test-sources/** 2.22.0.4796 6.1.0.3962 + 1.1.0.1340 1.25.1.3886 -Xmx512m sonar-java