Skip to content

Commit

Permalink
Add test coverage report (#113)
Browse files Browse the repository at this point in the history
Tasks derivated from Test now are able to generate code coverage. For it to happen, it is needed to set the extension property enableTestCodeCoverage to true. Coverage reports will be stored together with test reports. This is available only for Unity 2019.4 and older versions.

Co-authored-by: Joaquim Neto <joaquim.neto@wooga.net>
  • Loading branch information
Joaquimmnetto and Joaquimmnetto committed Aug 20, 2021
1 parent 9c3bd81 commit 51f0b25
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -167,23 +167,27 @@ class UnityPluginIntegrationSpec extends UnityIntegrationSpec {
query.matches(result, testValue)

where:
property | method | rawValue | expectedValue | type | location
"unityPath" | _ | _ | UnityPluginConventions.getPlatformUnityPath().absolutePath | "Provider<RegularFile>" | PropertyLocation.none
"unityPath" | _ | osPath("/foo/bar/unity1") | _ | _ | PropertyLocation.environment
"unityPath" | _ | osPath("/foo/bar/unity2") | _ | _ | PropertyLocation.property
"unityPath" | "setUnityPath" | osPath("/foo/bar/unity3") | _ | "Provider<RegularFile>" | PropertyLocation.script
"unityPath" | "unityPath.set" | osPath("/foo/bar/unity4") | _ | "Provider<RegularFile>" | PropertyLocation.script

"defaultBuildTarget" | _ | _ | null | _ | PropertyLocation.none
"autoActivateUnity" | _ | _ | true | Boolean | PropertyLocation.none
"autoReturnLicense" | _ | _ | true | Boolean | PropertyLocation.none
"logCategory" | _ | _ | "unity" | "Property<String>" | PropertyLocation.none
"batchModeForEditModeTest" | _ | _ | true | Boolean | PropertyLocation.none
"batchModeForPlayModeTest" | _ | _ | true | Boolean | PropertyLocation.none

"assetsDir" | _ | _ | osPath("#projectDir#/Assets") | "Provider<Directory>" | PropertyLocation.none
"logsDir" | _ | _ | osPath("#projectDir#/build/logs") | "Provider<Directory>" | PropertyLocation.none
"reportsDir" | _ | _ | osPath("#projectDir#/build/reports") | "Provider<Directory>" | PropertyLocation.none
property | method | rawValue | expectedValue | type | location
"unityPath" | _ | _ | UnityPluginConventions.getPlatformUnityPath().absolutePath | "Provider<RegularFile>" | PropertyLocation.none
"unityPath" | _ | osPath("/foo/bar/unity1") | _ | _ | PropertyLocation.environment
"unityPath" | _ | osPath("/foo/bar/unity2") | _ | _ | PropertyLocation.property
"unityPath" | "setUnityPath" | osPath("/foo/bar/unity3") | _ | "Provider<RegularFile>" | PropertyLocation.script
"unityPath" | "unityPath.set" | osPath("/foo/bar/unity4") | _ | "Provider<RegularFile>" | PropertyLocation.script

"defaultBuildTarget" | _ | _ | null | _ | PropertyLocation.none
"autoActivateUnity" | _ | _ | true | Boolean | PropertyLocation.none
"autoReturnLicense" | _ | _ | true | Boolean | PropertyLocation.none
"logCategory" | _ | _ | "unity" | "Property<String>" | PropertyLocation.none
"batchModeForEditModeTest" | _ | _ | true | Boolean | PropertyLocation.none
"batchModeForPlayModeTest" | _ | _ | true | Boolean | PropertyLocation.none

"assetsDir" | _ | _ | osPath("#projectDir#/Assets") | "Provider<Directory>" | PropertyLocation.none
"logsDir" | _ | _ | osPath("#projectDir#/build/logs") | "Provider<Directory>" | PropertyLocation.none
"reportsDir" | _ | _ | osPath("#projectDir#/build/reports") | "Provider<Directory>" | PropertyLocation.none
"enableTestCodeCoverage" | _ | _ | false | Boolean | PropertyLocation.none
"enableTestCodeCoverage" | "enableTestCodeCoverage.set" | true | _ | "Provider<Boolean>" | PropertyLocation.script
"enableTestCodeCoverage" | "setEnableTestCodeCoverage" | true | _ | Boolean | PropertyLocation.script
"enableTestCodeCoverage" | "setEnableTestCodeCoverage" | true | _ | "Provider<Boolean>" | PropertyLocation.script

value = (type != _) ? wrapValueBasedOnType(rawValue, type) : rawValue
providedValue = (location == PropertyLocation.script) ? type : value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@

package wooga.gradle.unity.tasks

import com.wooga.spock.extensions.unity.UnityPluginTestOptions
import kotlin.Unit
import spock.lang.Unroll
import wooga.gradle.unity.UnityPlugin
import wooga.gradle.unity.UnityTaskIntegrationSpec
import wooga.gradle.unity.models.UnityCommandLineOption
import wooga.gradle.unity.testutils.GradleRunResult
import wooga.gradle.unity.utils.ProjectSettingsFile

class TestTaskIntegrationSpec extends UnityTaskIntegrationSpec<Test> {
Expand Down Expand Up @@ -201,4 +205,55 @@ class TestTaskIntegrationSpec extends UnityTaskIntegrationSpec<Test> {
result.standardOutput.contains("PlayMode tests not activated")

}

@UnityPluginTestOptions(addMockTask = false, disableAutoActivateAndLicense = false)
def "sets up coverage arguments if code coverage is enabled"() {
given: "a build script with fake test unity location"
and: "enabled test code coverage on extension"
buildFile << """
${UnityPlugin.EXTENSION_NAME} {
enableTestCodeCoverage = true
}
"""
when:
def result = runTasksSuccessfully("test")

then:
def playModeResult = new GradleRunResult(":testPlayModeAndroid", result.standardOutput)
matchesExpectedCoverageArgs(playModeResult)
def editModeResult = new GradleRunResult(":testEditModeAndroid", result.standardOutput)
matchesExpectedCoverageArgs(editModeResult)
}

@UnityPluginTestOptions(addMockTask = false, disableAutoActivateAndLicense = false)
def "doesnt sets up coverage arguments if code coverage is disabled"() {
given: "a build script with fake test unity location"
and: "enabled test code coverage on extension"
buildFile << """
${UnityPlugin.EXTENSION_NAME} {
enableTestCodeCoverage.set(false)
}
"""
when:
def result = runTasksSuccessfully("test")

then:
def playModeResult = new GradleRunResult(":testPlayModeAndroid", result.standardOutput)
!playModeResult.args.contains("-enableCodeCoverage")
def editModeResult = new GradleRunResult(":testEditModeAndroid", result.standardOutput)
!editModeResult.args.contains("-enableCodeCoverage")
}

boolean matchesExpectedCoverageArgs(GradleRunResult taskResult) {
return taskResult.args.contains("-enableCodeCoverage") &&
taskResult.args.contains("-coverageResultsPath") &&
taskResult.argValueMatches("-coverageResultsPath") { String value ->
new File(value) == new File(projectDir, "build/reports/unity")
} &&
taskResult.args.contains("-coverageOptions") &&
taskResult.argValueMatches("-coverageOptions") {it == "generateAdditionalMetrics"} &&
taskResult.args.contains("-debugCodeOptimization")

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package wooga.gradle.unity.testutils

import org.gradle.internal.impldep.org.apache.commons.lang.StringUtils

class GradleRunResult {
private final ArrayList<String> args;
private final Map<String, String> envs;

GradleRunResult(String stdOutput) {
this(null, stdOutput)
}

GradleRunResult(String task, String stdOutput) {
if(task != null) {
stdOutput = taskLog(task, stdOutput)
}
this.args = loadArgs(stdOutput)
this.envs = loadEnvs(stdOutput)
}

ArrayList<String> getArgs() {
return args
}

Map<String, String> getEnvs() {
return envs
}

boolean argValueMatches(String key, Closure matcher) {
def argIndex = args.indexOf(key)
def value = args[argIndex+1]
return matcher(value)
}

private static String taskLog(String task, String stdOutput) {
String taskString = "> Task ${task}"
int taskBeginIdx = stdOutput.indexOf(taskString) + taskString.length()
String taskTail = stdOutput.substring(taskBeginIdx)
int taskEndIdx = taskTail.indexOf("> Task")
def logs = taskTail.substring(0, taskEndIdx)
return logs
}

private static ArrayList<String> loadArgs(String stdOutput) {
def argumentsStartToken = "[ARGUMENTS]:"
def lastExecutionOffset = stdOutput.lastIndexOf(argumentsStartToken)
if(lastExecutionOffset < 0) {
System.out.println(stdOutput)
throw new IllegalArgumentException("couldn't find arguments list in stdout")
}
def lastExecTailString = stdOutput.substring(lastExecutionOffset)
def argsString = substringBetween(lastExecTailString, argumentsStartToken, "Mock Unity Started").
replace(argumentsStartToken, "")
def parts = argsString.split(" ").
findAll {!StringUtils.isEmpty(it) }.collect{ it.trim() }
return parts
}

private static Map<String, String> loadEnvs(String stdOutput) {
String environmentStartToken = "[ENVIRONMENT]:"
def argsString = substringBetween(stdOutput, environmentStartToken, "[ARGUMENTS]").
replace(environmentStartToken, "")
def parts = argsString.split(System.lineSeparator()).
findAll {!StringUtils.isEmpty(it) }.collect{ it.trim() }
return parts.collectEntries {
return it.split("=", 2)
}
}
private static String substringBetween(String base, String from, String to) {
def customArgsIndex = base.indexOf(from)
def tailString = base.substring(customArgsIndex)
def endIndex = tailString.indexOf(to)
return tailString.substring(0, endIndex)
}
}

7 changes: 7 additions & 0 deletions src/main/groovy/wooga/gradle/unity/UnityPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class UnityPlugin implements Plugin<Project> {
def file = new File(extension.projectDirectory.get().asFile.path, "ProjectSettings/ProjectSettings.asset")
return new ProjectSettingsFile(file)
}))
extension.enableTestCodeCoverage.convention(UnityPluginConventions.enableTestCodeCoverage.getBooleanValueProvider(project))
}

private static void configureUnityTasks(UnityPluginExtension extension, final Project project) {
Expand Down Expand Up @@ -214,6 +215,12 @@ class UnityPlugin implements Plugin<Project> {
}
t.batchMode.set(testBatchModeProvider)
t.reports.xml.outputLocation.convention(extension.reportsDir.file(t.name + "/" + t.name + "." + reports.xml.name))
t.enableCodeCoverage.convention(extension.enableTestCodeCoverage)
t.coverageResultsPath.convention(extension.enableTestCodeCoverage.map {
return it? extension.reportsDir.getOrElse(null)?.asFile?.absolutePath: null
})
t.coverageOptions.convention(extension.enableTestCodeCoverage.map {it? "generateAdditionalMetrics" : null})
t.debugCodeOptimization.convention(extension.enableTestCodeCoverage) //needed from 2020.1 and on for coverage
})

// Make sure the lifecycle check task depends on our test task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ class UnityPluginConventions implements PlatformUtilsImpl {
* Targets to generate tests for
*/
static final PropertyLookup testBuildTargets = new PropertyLookup("UNITY_TEST_BUILD_TARGETS", "unity.testBuildTargets", null)
/**
* Targets to generate tests for
*/
static final PropertyLookup enableTestCodeCoverage = new PropertyLookup("UNITY_ENABLE_TEST_COVERAGE", "unity.enableTestCodeCoverage", false)

/**
* The path to the Unity license directory
Expand Down
19 changes: 17 additions & 2 deletions src/main/groovy/wooga/gradle/unity/UnityPluginExtension.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@

package wooga.gradle.unity

import org.codehaus.groovy.runtime.StringGroovyMethods

import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Internal
import wooga.gradle.unity.models.BuildTarget
import wooga.gradle.unity.traits.APICompatibilityLevelSpec
import wooga.gradle.unity.traits.UnityAuthenticationSpec
import wooga.gradle.unity.traits.UnityLicenseSpec
Expand Down Expand Up @@ -191,6 +190,22 @@ trait UnityPluginExtension implements UnitySpec,
testBuildTargets.addAll(targets)
}

private final Property<Boolean> enableTestCodeCoverage = objects.property(Boolean)

/**
* @return true if code coverage is enabled for tests, false otherwise.
*/
Property<Boolean> getEnableTestCodeCoverage() {
return enableTestCodeCoverage
}

void setEnableTestCodeCoverage(Boolean value) {
enableTestCodeCoverage.set(value)
}

void setEnableTestCodeCoverage(Provider<Boolean> value) {
enableTestCodeCoverage.set(value)
}

/**
* Returns a {@link java.util.Set} of {@link wooga.gradle.unity.models.BuildTarget} objects to construct unity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,29 @@ enum UnityCommandLineOption {
/**
* Enables code coverage and allows access to the Coverage API.
*/
enableCodeCoverage("-enableCodeCoverage")
enableCodeCoverage("-enableCodeCoverage"),
/**
* Sets the location where the coverage results and report will be saved to.
* The default location is the project's path.
* https://docs.unity3d.com/Packages/com.unity.testtools.codecoverage@1.1/manual/CoverageBatchmode.html
*/
coverageResultsPath("-coverageResultsPath", true),
/**
* Sets the location where the coverage report history will be saved to.
* The default location is the project's path.
* https://docs.unity3d.com/Packages/com.unity.testtools.codecoverage@1.1/manual/CoverageBatchmode.html
*/
coverageHistoryPath("-coverageHistoryPath", true),
/**
* Passes extra options. This is semicolon separated.
* https://docs.unity3d.com/Packages/com.unity.testtools.codecoverage@1.1/manual/CoverageBatchmode.html
*/
coverageOptions("-coverageOptions", true),
/**
* Enables debug code optimization mode, overriding the current default code optimization mode for the session.
* Needed for code coverage on 2020.1 and older versions.
*/
debugCodeOptimization("-debugCodeOptimization")

private final String flag
private final Boolean hasArguments
Expand Down
6 changes: 5 additions & 1 deletion src/main/groovy/wooga/gradle/unity/tasks/Test.groovy
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package wooga.gradle.unity.tasks


import org.gradle.api.provider.Property
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.StopExecutionException
import org.gradle.internal.reflect.Instantiator
Expand Down Expand Up @@ -65,6 +65,10 @@ abstract class Test extends UnityTask implements UnityTestSpec {
throw new StopExecutionException("PlayMode tests not activated for this project. Please activate PlayMode tests first")
}

if(unityVersion.majorVersion >= 2018 && unityVersion.minorVersion >= 3) {
enableCodeCoverage.convention(false)
}

} else {
throw new StopExecutionException("Unit test feature not supported with unity version: ${unityVersion.toString()}")
}
Expand Down
Loading

0 comments on commit 51f0b25

Please sign in to comment.