From 7d9084169d0617781e5ecd37ca9c4c33951f79a8 Mon Sep 17 00:00:00 2001 From: Joaquim Alvino de Mesquita Neto Date: Thu, 18 Apr 2024 16:40:56 +0200 Subject: [PATCH] initial implementation for wooga.gradle.unity-sonarqube --- build.gradle | 8 ++ ...tySonarqubeExtensionIntegrationSpec.groovy | 51 +++++++ ...UnitySonarqubePluginIntegrationSpec.groovy | 84 ++++++++++++ .../unitysonar/UnitySonarqubeExtension.groovy | 43 ++++++ .../unitysonar/UnitySonarqubePlugin.groovy | 126 ++++++++++++++++++ .../UnitySonarqubePluginConventions.groovy | 19 +++ .../unity-sonarqube.project-fixes.props | 16 +++ .../UnitySonarqubePluginSpec.groovy | 104 +++++++++++++++ 8 files changed, 451 insertions(+) create mode 100644 src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtensionIntegrationSpec.groovy create mode 100644 src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginIntegrationSpec.groovy create mode 100644 src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtension.groovy create mode 100644 src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePlugin.groovy create mode 100644 src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginConventions.groovy create mode 100644 src/main/resources/unity-sonarqube.project-fixes.props create mode 100644 src/test/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginSpec.groovy diff --git a/build.gradle b/build.gradle index 6bde4e1..c57418d 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,12 @@ gradlePlugin { description = 'This plugin provides tools to run sonarqube scans for .NET solutions' implementationClass = 'wooga.gradle.dotnetsonar.DotNetSonarqubePlugin' } + unitySonarqube { + id = 'net.wooga.unity-sonarqube' + displayName = 'Sonarqube plugin for wooga unity projects' + description = 'This plugin provides tools to run sonarqube scans for .NET unity solutions' + implementationClass = 'wooga.gradle.unitysonar.UnitySonarqubePlugin' + } } } @@ -62,6 +68,8 @@ dependencies { api 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' implementation 'net.wooga.gradle:dotnet:0.3.0' implementation 'net.wooga.gradle:github:[3,4[' + implementation 'net.wooga.gradle:unity:[4,5[' + implementation 'net.wooga.gradle:asdf:[1,2[' testImplementation 'com.wooga.spock.extensions:spock-github-extension:0.4.0' testImplementation 'org.ajoberstar.grgit:grgit-core:4.+' testImplementation 'com.wooga.gradle:gradle-commons-test:[2,3)' diff --git a/src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtensionIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtensionIntegrationSpec.groovy new file mode 100644 index 0000000..e9ca34f --- /dev/null +++ b/src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtensionIntegrationSpec.groovy @@ -0,0 +1,51 @@ +package wooga.gradle.unitysonar + +import com.wooga.gradle.test.IntegrationSpec +import com.wooga.gradle.test.PropertyLocation +import com.wooga.gradle.test.writers.PropertyGetterTaskWriter +import com.wooga.gradle.test.writers.PropertySetterWriter +import spock.lang.Unroll + +import static com.wooga.gradle.test.writers.PropertySetInvocation.getAssignment +import static com.wooga.gradle.test.writers.PropertySetInvocation.getNone +import static com.wooga.gradle.test.writers.PropertySetInvocation.getProviderSet +import static com.wooga.gradle.test.writers.PropertySetInvocation.getSetter + +class UnitySonarqubeExtensionIntegrationSpec extends IntegrationSpec { + + def setup() { + buildFile << "${applyPlugin(UnitySonarqubePlugin)}\n" + } + + @Unroll("can set property unitySonarqube.#property with #invocation and type #type") + def "can set property on unitySonarqube extension with build.gradle"() { + given: + when: + set.location = invocation == none ? PropertyLocation.none : set.location + def propertyQuery = runPropertyQuery(get, set) + + then: + propertyQuery.matches(rawValue) + + where: + property | invocation | rawValue | type + "buildDotnetVersion" | providerSet | "7.0.200" | "Provider" + "buildDotnetVersion" | assignment | "7.0.200" | "Provider" + "buildDotnetVersion" | assignment | "7.0.200" | "String" + "buildDotnetVersion" | setter | "7.0.200" | "Provider" + "buildDotnetVersion" | setter | "7.0.200" | "String" + "buildDotnetVersion" | none | "7.0.100" | "String" + + "buildDotnetExecutable" | providerSet | "dot_net" | "Provider" + "buildDotnetExecutable" | assignment | "dir/dot_net" | "Provider" + "buildDotnetExecutable" | assignment | "dot_net" | "String" + "buildDotnetExecutable" | setter | "d/sd/dot_net" | "Provider" + "buildDotnetExecutable" | setter | "dot_net" | "String" + "buildDotnetExecutable" | none | null | "String" + + set = new PropertySetterWriter("unitySonarqube", property) + .set(rawValue, type) + .toScript(invocation) + get = new PropertyGetterTaskWriter(set) + } +} diff --git a/src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginIntegrationSpec.groovy new file mode 100644 index 0000000..dfe3491 --- /dev/null +++ b/src/integrationTest/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginIntegrationSpec.groovy @@ -0,0 +1,84 @@ +package wooga.gradle.unitysonar + +import com.wooga.gradle.test.IntegrationSpec +import com.wooga.gradle.test.run.result.GradleRunResult +import com.wooga.gradle.test.writers.PropertyGetterTaskWriter +import com.wooga.gradle.test.writers.PropertySetterWriter + +class UnitySonarqubePluginIntegrationSpec extends IntegrationSpec { + + def setup() { + buildFile << "${applyPlugin(UnitySonarqubePlugin)}\n" + } + + def "sonarqube task run tests and sonar build"() { + given: "applied unity-sonarqube plugin" + + when: + def result = runTasks("sonarqube", "--dry-run") + + then: + def run = new GradleRunResult(result) + run["test"] + run["sonarBuildUnity"] + run["sonarqube"].wasExecutedAfter("test") + run["sonarqube"].wasExecutedAfter("sonarBuildUnity") + } + + def "sonarBuildUnity task runs in expected order"() { + given: "applied unity-sonarqube plugin" + + when: + def result = runTasks("sonarBuildUnity", "--dry-run") + + then: + def run = new GradleRunResult(result) + run["asdfBinstubsDotnet"].wasExecutedBefore("sonarBuildUnity") + run["generateSolution"].wasExecutedBefore("sonarBuildUnity") + run["sonarScannerBegin"].wasExecutedBefore("sonarBuildUnity") + run["sonarScannerEnd"].wasExecutedAfter("sonarBuildUnity") + } + + def "sonarqube build task runs after unity tests"() { + given: "applied sonarqube plugin" + + when: + def result = runTasks("sonarBuildUnity", "test", "--dry-run") + + then: + def run = new GradleRunResult(result) + run["test"].wasExecutedBefore("sonarBuildUnity") + } + + def "#task uses asdf-installed dotnet as default executable"() { + given: "applied unity-sonarqube plugin" + when: + getter.write(buildFile) + def tasksResult = runTasks("asdfBinstubsDotnet", getter.taskName) + def query = getter.generateQuery(this, tasksResult) + + then: + query.matches(new File(projectDir, "bin/dotnet").absolutePath) + + where: + task << ["sonarBuildUnity", "sonarScannerBegin", "sonarScannerEnd"] + getter = new PropertyGetterTaskWriter("${task}.executable") + } + + def "#task uses extension-set dotnet as executable"() { + given: "applied unity-sonarqube plugin" + when: + def query = runPropertyQuery(getter, setter) + + then: + query.matches(expectedExecutable) + + where: + task = "sonarBuildUnity" + expectedExecutable = "folder/dot_net" + getter = new PropertyGetterTaskWriter("${task}.executable") + setter = new PropertySetterWriter("unitySonarqube", "buildDotnetExecutable") + .set(expectedExecutable, String) + } + +} diff --git a/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtension.groovy b/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtension.groovy new file mode 100644 index 0000000..ffa3aec --- /dev/null +++ b/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubeExtension.groovy @@ -0,0 +1,43 @@ +package wooga.gradle.unitysonar + +import com.wooga.gradle.BaseSpec +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional + +class UnitySonarqubeExtension implements BaseSpec { + + private final Property buildDotnetVersion = objects.property(String) + + @Input + @Optional + Property getBuildDotnetVersion() { + return buildDotnetVersion + } + + void setBuildDotnetVersion(String buildDotnetVersion) { + this.buildDotnetVersion.set(buildDotnetVersion) + } + + void setBuildDotnetVersion(Provider buildDotnetVersion) { + this.buildDotnetVersion.set(buildDotnetVersion) + } + + private final Property buildDotnetExecutable = objects.property(String) + + @Input + @Optional + Property getBuildDotnetExecutable() { + return buildDotnetExecutable + } + + void setBuildDotnetExecutable(String buildDotnetExecutable) { + this.buildDotnetExecutable.set(buildDotnetExecutable) + } + + void setBuildDotnetExecutable(Provider buildDotnetExecutable) { + this.buildDotnetExecutable.set(buildDotnetExecutable) + } + +} diff --git a/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePlugin.groovy b/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePlugin.groovy new file mode 100644 index 0000000..1e15499 --- /dev/null +++ b/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePlugin.groovy @@ -0,0 +1,126 @@ +package wooga.gradle.unitysonar + +import org.gradle.api.Action +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownTaskException +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.sonarqube.gradle.SonarQubeExtension +import org.sonarqube.gradle.SonarQubeProperties +import wooga.gradle.asdf.AsdfPlugin +import wooga.gradle.asdf.AsdfPluginExtension +import wooga.gradle.asdf.internal.ToolVersionInfo +import wooga.gradle.dotnetsonar.DotNetSonarqubePlugin +import wooga.gradle.dotnetsonar.SonarScannerExtension +import wooga.gradle.dotnetsonar.tasks.BuildSolution +import wooga.gradle.unity.UnityPlugin +import wooga.gradle.unity.UnityPluginExtension + +class UnitySonarqubePlugin implements Plugin { + + Project project + + @Override + void apply(Project project) { + this.project = project + + project.pluginManager.apply(UnityPlugin.class) + project.pluginManager.apply(DotNetSonarqubePlugin.class) + project.pluginManager.apply(AsdfPlugin.class) + + def unitySonarqube = createExtension("unitySonarqube") + //uses same dotnet version for both sonarScanner and buildTask + def asdf = configureAsdfWithTool("dotnet", unitySonarqube.buildDotnetVersion) + def unity = configureUnityPlugin() + configureSonarScanner(asdf) + configureSonarqubePlugin(unity) + + + def unityTestTask = project.tasks.named(UnityPlugin.Tasks.test.toString()) + def createSolutionTask = project.tasks.named(UnityPlugin.Tasks.generateSolution.toString()) + + def sonarBuild = project.tasks.register("sonarBuildUnity", BuildSolution) { it -> + it.dependsOn(createSolutionTask) + it.mustRunAfter(unityTestTask) + + def buildDotnetDefault = unitySonarqube.buildDotnetExecutable.orElse(asdf.getTool("dotnet").getExecutable("dotnet")) + it.executableName.convention(buildDotnetDefault) + it.solution.convention(unity.projectDirectory.file("${project.name}.sln")) + it.environment.put("FrameworkPathOverride", unity.monoFrameworkDir.map { it.asFile.absolutePath }) + + def propsFix = createSolutionPropertyFixFile("/unity-sonarqube.project-fixes.props") + it.additionalArguments.add("/p:CustomBeforeMicrosoftCommonProps=${propsFix.absolutePath}") + } + + namedOrRegister("sonarqube", DefaultTask) { task -> + task.dependsOn(unityTestTask, sonarBuild) + } + } + + UnitySonarqubeExtension createExtension(String extensionName) { + def unitySonarqube = project.extensions.create(extensionName, UnitySonarqubeExtension) + unitySonarqube.buildDotnetVersion.convention(UnitySonarqubePluginConventions.buildDotnetVersion.getStringValueProvider(project)) + return unitySonarqube + } + + AsdfPluginExtension configureAsdfWithTool(String name, Provider version) { + def asdf = project.extensions.getByType(AsdfPluginExtension) + asdf.tool(new ToolVersionInfo(name, version)) + return asdf + } + + UnityPluginExtension configureUnityPlugin() { + def unityExt = project.extensions.findByType(UnityPluginExtension) + unityExt.enableTestCodeCoverage = true + return unityExt + } + + SonarScannerExtension configureSonarScanner(AsdfPluginExtension asdf) { + def sonarScanner = project.extensions.findByType(SonarScannerExtension) + + def asdfDotnet = asdf.getTool("dotnet") + sonarScanner.dotnetExecutable.convention(asdfDotnet.getExecutable("dotnet")) + return sonarScanner + } + + SonarQubeExtension configureSonarqubePlugin(UnityPluginExtension unity) { + def sonarExt = project.extensions.findByType(SonarQubeExtension) + sonarExt.properties({ + def assetsDir = unity.assetsDir.get().asFile + def reportsDir = unity.reportsDir.get().asFile + def relativeAssetsDir = project.projectDir.relativePath(assetsDir) + addPropertyIfNotExists(it, "sonar.cpd.exclusions", "${relativeAssetsDir}/**/Tests/**") + addPropertyIfNotExists(it, "sonar.coverage.exclusions", "${relativeAssetsDir}/**/Tests/**") + addPropertyIfNotExists(it, "sonar.exclusions", "${relativeAssetsDir}/Paket.Unity3D/**") + addPropertyIfNotExists(it, "sonar.cs.nunit.reportsPaths", "${reportsDir.path}/**/*.xml") + addPropertyIfNotExists(it, "sonar.cs.opencover.reportsPaths", "${reportsDir.path}/**/*.xml") + }) + return sonarExt + } + + protected static void addPropertyIfNotExists(SonarQubeProperties properties, String key, Object value) { + if (!properties.properties.containsKey(key)) { + properties.property(key, value) + } + } + + protected static File createSolutionPropertyFixFile(String resourceFileName) { + def propsFixResource = UnitySonarqubePlugin.class.getResourceAsStream(resourceFileName) + def propsFixTmpFile = File.createTempFile("unity-sonarqube-", ".project-fixes.props") + propsFixTmpFile.text = propsFixResource.text + return propsFixTmpFile + } + + protected TaskProvider namedOrRegister(String taskName, Class type = DefaultTask.class, Action configuration = { it -> }) { + try { + return project.tasks.named(taskName, type, configuration) + } catch (UnknownTaskException ignore) { + return project.tasks.register(taskName, type, configuration) + } + } + + +} diff --git a/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginConventions.groovy b/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginConventions.groovy new file mode 100644 index 0000000..57ae535 --- /dev/null +++ b/src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginConventions.groovy @@ -0,0 +1,19 @@ +package wooga.gradle.unitysonar + +import com.wooga.gradle.PropertyLookup + +class UnitySonarqubePluginConventions { + + static PropertyLookup buildDotnetVersion = new PropertyLookup( + "UNITY_SONARQUBE_BUILD_DOTNET_VERSION", + "unitySonarqube.buildDotnetVersion", + "7.0.100" + ) + + static PropertyLookup buildDotnetExecutable = new PropertyLookup( + "UNITY_SONARQUBE_BUILD_DOTNET_EXECUTABLE", + "unitySonarqube.buildDotnetExecutable", + null + ) + +} diff --git a/src/main/resources/unity-sonarqube.project-fixes.props b/src/main/resources/unity-sonarqube.project-fixes.props new file mode 100644 index 0000000..ab6d9c3 --- /dev/null +++ b/src/main/resources/unity-sonarqube.project-fixes.props @@ -0,0 +1,16 @@ + + + + + false + + + + + + + + true + + + \ No newline at end of file diff --git a/src/test/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginSpec.groovy b/src/test/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginSpec.groovy new file mode 100644 index 0000000..ac935d1 --- /dev/null +++ b/src/test/groovy/wooga/gradle/unitysonar/UnitySonarqubePluginSpec.groovy @@ -0,0 +1,104 @@ +package wooga.gradle.unitysonar + +import nebula.test.ProjectSpec +import org.gradle.api.DefaultTask +import org.gradle.api.Task +import spock.lang.Unroll +import wooga.gradle.dotnetsonar.SonarScannerExtension +import wooga.gradle.dotnetsonar.tasks.BuildSolution +import wooga.gradle.unity.UnityPlugin +import wooga.gradle.unity.UnityPluginExtension + +class UnitySonarqubePluginSpec extends ProjectSpec { + + public static final String PLUGIN_NAME = 'net.wooga.unity-sonarqube' + + @Unroll("creates the task #taskName") + def 'Creates needed tasks'(String taskName, Class taskType) { + given: + assert !project.plugins.hasPlugin(PLUGIN_NAME) + assert !project.tasks.findByName(taskName) + + when: + project.plugins.apply(PLUGIN_NAME) + Task task + project.afterEvaluate { + task = project.tasks.findByName(taskName) + } + + then: + project.evaluate() + taskType.isInstance(task) + + where: + taskName | taskType + "sonarqube" | DefaultTask + "sonarBuildUnity" | BuildSolution + } + + @Unroll("task #taskName has runtime dependencies") + def 'Task has runtime dependencies'(String taskName, String[] dependencies) { + given: + assert !project.plugins.hasPlugin(PLUGIN_NAME) + assert !project.tasks.findByName(taskName) + + when: + project.plugins.apply(PLUGIN_NAME) + + then: + Task task = project.tasks.findByName(taskName) + task.getTaskDependencies().getDependencies(task).findAll { t -> + dependencies.find { + depName -> t.name == depName + } + } + task.getFinalizedBy().getDependencies(task).collect{it.name } == finalizedBy + + where: + taskName | dependencies | finalizedBy + "sonarqube" | [UnityPlugin.Tasks.test.toString(), "sonarBuildUnity"] | [] + "sonarBuildUnity" | ["asdfBinstubsDotnet", "generateSolution", "sonarScannerBegin"] | ["sonarScannerEnd"] + } + + def "configures sonarqube extension"() { + given: "project without plugin applied" + assert !project.plugins.hasPlugin(PLUGIN_NAME) + + when: "applying atlas-build-unity plugin" + project.plugins.apply(PLUGIN_NAME) + project.evaluate() + + then: + def sonarExt = project.extensions.getByType(SonarScannerExtension) + def unityExt = project.extensions.getByType(UnityPluginExtension) + and: "sonarqube extension is configured with defaults" + def properties = sonarExt.sonarQubeProperties.get() + def reportsDir = unityExt.reportsDir.get().asFile.path + properties["sonar.exclusions"] == "Assets/Paket.Unity3D/**" + properties["sonar.cpd.exclusions"] == "Assets/**/Tests/**" + properties["sonar.coverage.exclusions"] == "Assets/**/Tests/**" + properties["sonar.cs.nunit.reportsPaths"] == "${reportsDir}/**/*.xml" + properties["sonar.cs.opencover.reportsPaths"] == "${reportsDir}/**/*.xml" + } + + def "configures sonarBuildUnity task"() { + given: "project without plugin applied" + assert !project.plugins.hasPlugin(PLUGIN_NAME) + and: "props file with fixes to run unity project on msbuild properly" + + when: "applying atlas-build-unity plugin" + project.plugins.apply(PLUGIN_NAME) + + then: + def unityExt = project.extensions.getByType(UnityPluginExtension) + def buildTask = project.tasks.getByName("sonarBuildUnity") as BuildSolution + + //Executable depends on asdf binstubs task being executed. Its tested on UnitySonarqubePluginIntegrationSpec. + buildTask.solution.get().asFile == new File(projectDir, "${project.name}.sln") + buildTask.environment.getting("FrameworkPathOverride").getOrElse(null) == + unityExt.monoFrameworkDir.map { it.asFile.absolutePath }.getOrElse(null) + buildTask.arguments.get().any { + it.startsWith("/p:CustomBeforeMicrosoftCommonProps=") && it.endsWith(".project-fixes.props") + } + } +}