Skip to content
This repository was archived by the owner on Feb 20, 2025. It is now read-only.

Commit c3ff8c9

Browse files
committed
Add log error handling to Unity task when the Unity process execution fails
1 parent 908ab9b commit c3ff8c9

File tree

8 files changed

+389
-48
lines changed

8 files changed

+389
-48
lines changed

src/integrationTest/groovy/wooga/gradle/unity/UnityPluginIntegrationSpec.groovy

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package wooga.gradle.unity
2020
import com.wooga.spock.extensions.unity.UnityPathResolution
2121
import com.wooga.spock.extensions.unity.UnityPluginTestOptions
2222
import spock.lang.Unroll
23-
import wooga.gradle.unity.models.BuildTarget
2423
import wooga.gradle.unity.models.UnityCommandLineOption
2524
import wooga.gradle.unity.tasks.Test
2625
import wooga.gradle.unity.utils.ProjectSettingsFile

src/integrationTest/groovy/wooga/gradle/unity/UnityTaskIntegrationSpec.groovy

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.wooga.spock.extensions.unity.UnityPathResolution
2121
import com.wooga.spock.extensions.unity.UnityPluginTestOptions
2222
import com.wooga.spock.extensions.uvm.UnityInstallation
2323
import org.gradle.api.logging.LogLevel
24+
import spock.lang.Ignore
2425
import spock.lang.IgnoreIf
2526
import spock.lang.Unroll
2627
import spock.util.environment.RestoreSystemProperties
@@ -386,4 +387,22 @@ abstract class UnityTaskIntegrationSpec<T extends UnityTask> extends UnityIntegr
386387
logFile.text.contains(mockUnityStartupMessage)
387388
}
388389

390+
@Ignore
391+
// TODO: How to make the task fail/throw within the exec block?
392+
def "task action does not invoke post execute if process didn't run"() {
393+
given: "a task that is definitely gonna fail"
394+
appendToSubjectTask("""
395+
environment = null
396+
""".stripIndent())
397+
398+
when:
399+
def result = runTasks(subjectUnderTestName)
400+
401+
then:
402+
!result.success
403+
outputContains(result, "${subjectUnderTestName}.preExecute")
404+
outputContains(result, "${subjectUnderTestName}.execute")
405+
!outputContains(result, "${subjectUnderTestName}.postExecute")
406+
}
407+
389408
}

src/main/groovy/wooga/gradle/unity/UnityPlugin.groovy

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,13 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin
3131
import wooga.gradle.unity.models.APICompatibilityLevel
3232
import wooga.gradle.unity.models.DefaultUnityAuthentication
3333
import wooga.gradle.unity.internal.DefaultUnityPluginExtension
34-
import wooga.gradle.unity.models.BuildTarget
3534
import wooga.gradle.unity.models.TestPlatform
3635
import wooga.gradle.unity.tasks.Activate
3736

3837
import wooga.gradle.unity.tasks.ReturnLicense
3938
import wooga.gradle.unity.tasks.SetAPICompatibilityLevel
4039
import wooga.gradle.unity.tasks.Test
4140
import wooga.gradle.unity.utils.ProjectSettingsFile
42-
import wooga.gradle.unity.utils.UnityTestTaskReport
43-
import wooga.gradle.unity.utils.UnityTestTaskReportsImpl
4441

4542
/**
4643
* A {@link org.gradle.api.Plugin} which provides tasks to run unity batch-mode commands.

src/main/groovy/wooga/gradle/unity/UnityPluginExtension.groovy

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import org.gradle.api.file.DirectoryProperty
2424
import org.gradle.api.provider.Property
2525
import org.gradle.api.provider.Provider
2626
import org.gradle.api.tasks.Internal
27-
import wooga.gradle.unity.models.BuildTarget
2827
import wooga.gradle.unity.traits.APICompatibilityLevelSpec
2928
import wooga.gradle.unity.traits.UnityAuthenticationSpec
3029
import wooga.gradle.unity.traits.UnityLicenseSpec

src/main/groovy/wooga/gradle/unity/UnityTask.groovy

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ import org.gradle.internal.io.LineBufferingOutputStream
2727
import org.gradle.internal.io.TextStream
2828
import org.gradle.process.ExecResult
2929
import org.gradle.process.ExecSpec
30+
import org.gradle.api.file.RegularFile
3031
import sun.reflect.misc.FieldUtil
3132
import wooga.gradle.unity.models.UnityCommandLineOption
3233
import wooga.gradle.unity.traits.ArgumentsSpec
3334
import wooga.gradle.unity.traits.UnityCommandLineSpec
3435
import wooga.gradle.unity.traits.UnitySpec
3536
import wooga.gradle.unity.utils.FileUtils
3637
import wooga.gradle.unity.utils.ForkTextStream
38+
import wooga.gradle.unity.utils.UnityLogErrorReader
3739
import wooga.gradle.unity.utils.UnityVersionManager
3840

3941
abstract class UnityTask extends DefaultTask
@@ -47,21 +49,24 @@ abstract class UnityTask extends DefaultTask
4749
// and also an additional one for our custom use
4850
wooga_gradle_unity_traits_ArgumentsSpec__arguments = project.provider({ getUnityCommandLineOptions() })
4951
wooga_gradle_unity_traits_ArgumentsSpec__additionalArguments = project.objects.listProperty(String)
50-
wooga_gradle_unity_traits_ArgumentsSpec__environment = project.objects.mapProperty(String, Object)
52+
wooga_gradle_unity_traits_ArgumentsSpec__environment = project.objects.mapProperty(String, Object)
5153
}
5254

5355
@TaskAction
5456
void exec() {
57+
// Invoked before the unity process
58+
logger.info("${name}.preExecute")
59+
preExecute()
60+
// Execute the unity process
61+
logger.info("${name}.execute")
5562
ExecResult execResult = project.exec(new Action<ExecSpec>() {
5663
@Override
5764
void execute(ExecSpec exec) {
5865

66+
// TODO: Should these be moved before preExecute?
5967
if (!unityPath.present) {
6068
throw new GradleException("Unity path is not set")
6169
}
62-
63-
preExecute()
64-
6570
def unityPath = unityPath.get().asFile.absolutePath
6671
def unityArgs = getAllArguments()
6772
def unityEnvironment = environment.get()
@@ -76,8 +81,11 @@ abstract class UnityTask extends DefaultTask
7681
}
7782
}
7883
})
79-
80-
execResult.assertNormalExitValue()
84+
if (execResult.exitValue != 0) {
85+
handleUnityProcessError(execResult)
86+
}
87+
// Invoked after the unity process (even if it failed)
88+
logger.info("${name}.postExecute")
8189
postExecute(execResult)
8290
}
8391

@@ -97,6 +105,30 @@ abstract class UnityTask extends DefaultTask
97105
protected void postExecute(ExecResult result) {
98106
}
99107

108+
/**
109+
* Invoked whenever the Unity process executed by the task exits with an error,
110+
* @param result The execution result of the Unity process
111+
*/
112+
protected void handleUnityProcessError(ExecResult result) {
113+
logger.error("Unity process failed with exit value ${result.exitValue}...")
114+
115+
// Look up the log
116+
if (!unityLogFile.isPresent()) {
117+
logger.warn("No log file was configured for the task ${this.name}")
118+
return
119+
}
120+
121+
File logFile = unityLogFile.get().asFile
122+
if (!logFile.exists()) {
123+
logger.warn("No log file was written for the task ${this.name}")
124+
return
125+
}
126+
127+
// TODO: Gracefully show the error here?
128+
def errorParse = UnityLogErrorReader.readErrorMessageFromLog(logFile)
129+
logger.error(errorParse.toString())
130+
}
131+
100132
@Internal
101133
protected ArtifactVersion getUnityVersion() {
102134
File file = unityPath.present ? unityPath.get().asFile : null

src/main/groovy/wooga/gradle/unity/traits/UnitySpec.groovy

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,15 @@
1616

1717
package wooga.gradle.unity.traits
1818

19-
import org.gradle.api.model.ObjectFactory
2019
import org.gradle.api.file.Directory
2120
import org.gradle.api.file.DirectoryProperty
2221
import org.gradle.api.file.RegularFile
2322
import org.gradle.api.file.RegularFileProperty
2423
import org.gradle.api.provider.Property
2524
import org.gradle.api.provider.Provider
26-
import org.gradle.api.provider.ProviderFactory
27-
import org.gradle.api.tasks.Input
28-
import org.gradle.api.tasks.InputDirectory
2925
import org.gradle.api.tasks.InputFile
3026
import org.gradle.api.tasks.Internal
31-
import org.gradle.api.tasks.Optional
3227
import org.gradle.api.tasks.OutputFile
33-
import org.gradle.api.tasks.SkipWhenEmpty
34-
import org.gradle.internal.impldep.org.eclipse.jgit.errors.NotSupportedException
35-
import wooga.gradle.unity.UnityPluginConventions
36-
import wooga.gradle.unity.models.BuildTarget
3728
import wooga.gradle.unity.utils.ProjectSettingsFile
3829

3930
import javax.inject.Inject

src/main/groovy/wooga/gradle/unity/utils/UnityLogErrorReader.groovy

Lines changed: 149 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,166 @@
1717

1818
package wooga.gradle.unity.utils
1919

20+
import java.util.regex.Matcher
21+
22+
enum UnityLogErrorType {
23+
/**
24+
* No error was found
25+
*/
26+
None,
27+
/**
28+
* The error could not be parsed
29+
*/
30+
Unknown,
31+
/**
32+
* The Unity scripts have compiler errors
33+
*/
34+
ScriptCompilerError,
35+
/**
36+
* The build failed
37+
*/
38+
BuildFailure,
39+
/**
40+
* Tests failed
41+
*/
42+
TestFailure
43+
}
44+
45+
class UnityLogErrorParse {
46+
UnityLogErrorType type
47+
String message
48+
List<CSharpFileCompilationResult> compilerOutput
49+
50+
@Override
51+
String toString() {
52+
def result = "${type}:"
53+
switch (type) {
54+
case UnityLogErrorType.ScriptCompilerError:
55+
compilerOutput.forEach({ l ->
56+
result += "\n${l.text}"
57+
})
58+
break
59+
}
60+
result
61+
}
62+
}
63+
64+
class CSharpFileCompilationResult {
65+
String text
66+
String filePath
67+
Integer line
68+
Integer column
69+
String level
70+
String code
71+
String message
72+
73+
CSharpFileCompilationResult(String filePath, Integer line, Integer column, String level, String code, String message) {
74+
this.filePath = filePath
75+
this.line = line
76+
this.column = column
77+
this.level = level
78+
this.code = code
79+
this.message = message
80+
}
81+
82+
CSharpFileCompilationResult(String text, String filePath, Integer line, Integer column, String level, String code, String message) {
83+
this.text = text
84+
this.filePath = filePath
85+
this.line = line
86+
this.column = column
87+
this.level = level
88+
this.code = code
89+
this.message = message
90+
}
91+
92+
CSharpFileCompilationResult() {
93+
}
94+
95+
// e.g: "A/B.cs(9,7): warning CS0105: WRONG!"
96+
static String pattern = /(?<filePath>.+)\((?<line>\d+),(?<column>\d+)\):\s(?<level>.*?)\s(?<code>.+):\s(?<message>.*)/
97+
98+
static CSharpFileCompilationResult Parse(String text) {
99+
Matcher matcher = (text =~ pattern)
100+
if (matcher.count == 0) {
101+
return null
102+
}
103+
104+
def (all, filePath, line, column, level, code, message) = matcher[0]
105+
CSharpFileCompilationResult result = new CSharpFileCompilationResult()
106+
result.text = all
107+
result.filePath = filePath
108+
result.line = Integer.parseInt(line)
109+
result.column = Integer.parseInt(column)
110+
result.level = level
111+
result.code = code
112+
result.message = message
113+
result
114+
}
115+
}
116+
20117
class UnityLogErrorReader {
21118

22-
static String DEFAULT_MESSAGE = "no error"
119+
static String compilerOutputStartMarker = "-----CompilerOutput:"
120+
static String compilerOutputEndMarker = "-----EndCompilerOutput"
121+
static String compilerErrorMarker = "Scripts have compiler errors."
122+
static String errorMarker = "Aborting batchmode due to failure:"
123+
static String dialogError = "Cancelling DisplayDialog:"
23124

24-
static String readErrorMessageFromLog(File logfile) {
25-
def message = DEFAULT_MESSAGE
26-
if(!logfile || !logfile.exists()) {
27-
return message
125+
static UnityLogErrorParse readErrorMessageFromLog(File logfile) {
126+
127+
UnityLogErrorParse parse = new UnityLogErrorParse()
128+
parse.type = UnityLogErrorType.None
129+
130+
if (!logfile || !logfile.exists()) {
131+
return parse
28132
}
29133

134+
boolean foundCompilerMarker = false
30135
boolean foundErrorMarker = false
31-
String errorMarker = "Aborting batchmode due to failure:"
32-
String dialogError = "Cancelling DisplayDialog:"
136+
parse.compilerOutput = []
33137
String line
138+
Integer lineNumber = 0
139+
140+
// Read through the log file
34141
logfile.withReader { reader ->
35-
while ((line = reader.readLine())!=null) {
36-
if(foundErrorMarker) {
37-
message = line
38-
break
39-
}
40-
if(line.startsWith(errorMarker)) {
41-
foundErrorMarker = true
42-
}
142+
while ((line = reader.readLine()) != null) {
143+
lineNumber++
43144

44-
if(line.startsWith(dialogError)) {
45-
message = line.replace(dialogError, "").trim()
46-
break
145+
// If inside a compiler marker, parse the compiler output
146+
if (foundCompilerMarker) {
147+
// Finished reading compiler output
148+
if (line.startsWith(compilerOutputEndMarker)) {
149+
foundCompilerMarker = false
150+
}
151+
// Record all warnings/errors
152+
else {
153+
CSharpFileCompilationResult fileCompilationResult = CSharpFileCompilationResult.Parse(line)
154+
if (fileCompilationResult != null) {
155+
parse.compilerOutput.add(fileCompilationResult)
156+
}
157+
}
158+
} else if (foundErrorMarker) {
159+
if (line.startsWith(compilerErrorMarker)) {
160+
parse.type = UnityLogErrorType.ScriptCompilerError
161+
break
162+
} else {
163+
parse.message = line
164+
}
165+
}
166+
// Look for markers
167+
else {
168+
// The error marker is found near the end of the log output
169+
if (line.startsWith(errorMarker)) {
170+
parse.type = UnityLogErrorType.Unknown
171+
foundErrorMarker = true
172+
}
173+
// Started reading through C# compiler output
174+
else if (line.startsWith(compilerOutputStartMarker)) {
175+
foundCompilerMarker = true
176+
}
47177
}
48178
}
49179
}
50-
51-
message
180+
parse
52181
}
53182
}

0 commit comments

Comments
 (0)