Skip to content

Commit

Permalink
initial implementation for wooga.gradle.unity-sonarqube
Browse files Browse the repository at this point in the history
  • Loading branch information
Joaquimmnetto committed Apr 19, 2024
1 parent a09b9ae commit 7d90841
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 0 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
}

Expand All @@ -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)'
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>"
"buildDotnetVersion" | assignment | "7.0.200" | "Provider<String>"
"buildDotnetVersion" | assignment | "7.0.200" | "String"
"buildDotnetVersion" | setter | "7.0.200" | "Provider<String>"
"buildDotnetVersion" | setter | "7.0.200" | "String"
"buildDotnetVersion" | none | "7.0.100" | "String"

"buildDotnetExecutable" | providerSet | "dot_net" | "Provider<String>"
"buildDotnetExecutable" | assignment | "dir/dot_net" | "Provider<String>"
"buildDotnetExecutable" | assignment | "dot_net" | "String"
"buildDotnetExecutable" | setter | "d/sd/dot_net" | "Provider<String>"
"buildDotnetExecutable" | setter | "dot_net" | "String"
"buildDotnetExecutable" | none | null | "String"

set = new PropertySetterWriter("unitySonarqube", property)
.set(rawValue, type)
.toScript(invocation)
get = new PropertyGetterTaskWriter(set)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
@@ -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<String> buildDotnetVersion = objects.property(String)

@Input
@Optional
Property<String> getBuildDotnetVersion() {
return buildDotnetVersion
}

void setBuildDotnetVersion(String buildDotnetVersion) {
this.buildDotnetVersion.set(buildDotnetVersion)
}

void setBuildDotnetVersion(Provider<String> buildDotnetVersion) {
this.buildDotnetVersion.set(buildDotnetVersion)
}

private final Property<String> buildDotnetExecutable = objects.property(String)

@Input
@Optional
Property<String> getBuildDotnetExecutable() {
return buildDotnetExecutable
}

void setBuildDotnetExecutable(String buildDotnetExecutable) {
this.buildDotnetExecutable.set(buildDotnetExecutable)
}

void setBuildDotnetExecutable(Provider<String> buildDotnetExecutable) {
this.buildDotnetExecutable.set(buildDotnetExecutable)
}

}
126 changes: 126 additions & 0 deletions src/main/groovy/wooga/gradle/unitysonar/UnitySonarqubePlugin.groovy
Original file line number Diff line number Diff line change
@@ -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 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<String> 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 <T extends Task> TaskProvider<T> namedOrRegister(String taskName, Class<T> type = DefaultTask.class, Action<T> configuration = { it -> }) {
try {
return project.tasks.named(taskName, type, configuration)
} catch (UnknownTaskException ignore) {
return project.tasks.register(taskName, type, configuration)
}
}


}
Original file line number Diff line number Diff line change
@@ -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
)

}
16 changes: 16 additions & 0 deletions src/main/resources/unity-sonarqube.project-fixes.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- Our wooga projects get falsely flagged as test projects due to `nunit` reference. -->
<SonarQubeTestProject>false</SonarQubeTestProject>
<!-- Unity default .sln export adds the Unity installed CSC tools which have issues to work with `.editoconfig` -->
<CscToolPath></CscToolPath>
<CscToolExe></CscToolExe>
</PropertyGroup>
<ItemGroup>
<!-- Unity 2020 default export disables all it's references. (known bug: https://issuetracker.unity3d.com/issues/referenceoutputassembly-key-is-set-to-false-in-project-references) -->
<ProjectReference Update="*.csproj">
<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>
Loading

0 comments on commit 7d90841

Please sign in to comment.