Skip to content

Commit 35fedc9

Browse files
romanralexey1312claude
authored
#49: Add PhaseScriptExecution failure handling and corresponding tests (#50)
* #49: Add PhaseScriptExecution failure handling and corresponding tests * refactor: Clean up PhaseScriptExecution error handling - Remove unused phaseScriptExecutionFailedRegex variable - Fix typo: "exitcode" → "exit code" in test - Optimize array access using errors.indices.last - Remove markdown artifacts from test file - Apply swift-format All 247 tests passing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: alexey1312 <alexey1312ru@gmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent f67dca6 commit 35fedc9

File tree

2 files changed

+295
-3
lines changed

2 files changed

+295
-3
lines changed

Sources/OutputParser.swift

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,48 @@ class OutputParser {
349349
) -> BuildResult {
350350
resetState()
351351
shouldParseBuildInfo = printBuildInfo
352-
let lines = input.split(separator: "\n", omittingEmptySubsequences: false)
352+
let lines = input.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
353+
354+
for (index, line) in lines.enumerated() {
355+
parseLine(line)
356+
357+
// Handle PhaseScriptExecution failures with context from preceding lines
358+
if line.contains("Command PhaseScriptExecution failed with a nonzero exit") {
359+
// Look back for relevant context (skip unrelated warnings and metadata)
360+
var contextLines: [String] = []
361+
let startIndex = max(0, index - 3) // Look back a few lines
362+
for contextIdx in startIndex ..< index {
363+
let contextLine = lines[contextIdx].trimmingCharacters(in: .whitespaces)
364+
365+
// Skip empty lines and build metadata warnings
366+
if contextLine.isEmpty || contextLine.hasPrefix("Warning:")
367+
|| contextLine.hasPrefix("Run script build phase")
368+
{
369+
continue
370+
}
353371

354-
for line in lines {
355-
parseLine(String(line))
372+
// Skip lines that contain Xcode build phase info that's not error-related
373+
if contextLine.contains(": warning:") && !contextLine.contains("error:") {
374+
continue
375+
}
376+
377+
// Include all other lines as they're likely actual error context
378+
contextLines.append(contextLine)
379+
}
380+
381+
// Combine context with failure message, using spaces as separator
382+
if !contextLines.isEmpty, let lastIndex = errors.indices.last,
383+
errors[lastIndex].message == line
384+
{
385+
let combinedMessage = contextLines.joined(separator: " ") + " " + line
386+
// Update the last error (which was just added by parseLine) with combined message
387+
errors[lastIndex] = BuildError(
388+
file: nil,
389+
line: nil,
390+
message: combinedMessage
391+
)
392+
}
393+
}
356394
}
357395

358396
// If warnings-as-errors is enabled, convert warnings to errors
@@ -988,6 +1026,11 @@ class OutputParser {
9881026
return BuildError(file: nil, line: nil, message: message)
9891027
}
9901028

1029+
// Pattern: Command PhaseScriptExecution failed with a nonzero exit code
1030+
if line.contains("Command PhaseScriptExecution failed with a nonzero exit") {
1031+
return BuildError(file: nil, line: nil, message: line)
1032+
}
1033+
9911034
return nil
9921035
}
9931036

Tests/BuildPhasesTest.swift

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import XCTest
2+
@testable import xcsift
3+
4+
class BuildPhasesTest: XCTestCase {
5+
let parser = OutputParser()
6+
7+
func testPhaseScriptExecutionFailureBasic() {
8+
let output = """
9+
/bin/sh -c /Users/dhavalkansara/Library/Developer/Xcode/DerivedData/AFEiOS-gctxucyuhlhesnfkbuxfswkozboo/Build/Intermediates.noindex/AFEiOS.build/Debug-iphoneos/AFEiOS.build/Script-19DAA30A22C0FB0100A039E2.sh
10+
The path lib/main.dart does not exist
11+
The path does not exist
12+
Command PhaseScriptExecution failed with a nonzero exit code
13+
"""
14+
15+
let result = parser.parse(input: output)
16+
17+
XCTAssertEqual(result.summary.errors, 1, "Should detect PhaseScriptExecution failure")
18+
XCTAssertFalse(result.errors.isEmpty, "Should have at least one error")
19+
20+
let error = result.errors[0]
21+
XCTAssertNil(error.file, "PhaseScriptExecution error should not have file")
22+
XCTAssertNil(error.line, "PhaseScriptExecution error should not have line number")
23+
XCTAssertTrue(
24+
error.message.contains("Command PhaseScriptExecution failed"),
25+
"Error message should contain failure indicator"
26+
)
27+
XCTAssertTrue(
28+
error.message.contains("The path lib/main.dart does not exist"),
29+
"Error message should include context from preceding lines"
30+
)
31+
}
32+
33+
func testPhaseScriptExecutionWithHermesFramework() {
34+
let output = """
35+
Run script build phase '[CP-User] [Hermes] Replace Hermes for the right configuration, if needed' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase.
36+
PhaseScriptExecution [CP-User]\\ [Hermes]\\ Replace\\ Hermes\\ for\\ the\\ right\\ configuration,\\ if\\ needed /Library/Developer/Xcode/DerivedData/myProjectName-gzdlehmipieiindfjyfrhhcjupam/Build/Intermediates.noindex/ArchiveIntermediates/myProjectName/IntermediateBuildFilesPath/Pods.build/Release-iphoneos/hermes-engine.build/Script-46EB2E0002C950.sh (in target 'hermes-engine' from project 'Pods')
37+
Node found at: /var/folders/d5/f1gffcfx27ngwvmw8v8jdm7m0000gn/T/yarn--1704767526546-0.12516067745295967/node
38+
/Library/Developer/Xcode/DerivedData/myProjectName-gzdlehmipieiindfjyfrhhcjupam/Build/Intermediates.noindex/ArchiveIntermediates/myProjectName/IntermediateBuildFilesPath/Pods.build/Release-iphoneos/hermes-engine.build/Script-46EB2E0002C950.sh: line 9: /var/folders/d5/f1gffcfx27ngwvmw8v8jdm7m0000gn/T/yarn--1704767526546-0.12516067745295967/node: No such file or directory
39+
Command PhaseScriptExecution failed with a nonzero exit code
40+
"""
41+
42+
let result = parser.parse(input: output)
43+
44+
XCTAssertEqual(result.summary.errors, 1, "Should detect PhaseScriptExecution failure for Hermes")
45+
XCTAssertFalse(result.errors.isEmpty, "Should have at least one error")
46+
47+
let error = result.errors[0]
48+
XCTAssertTrue(
49+
error.message.contains("Command PhaseScriptExecution failed"),
50+
"Error message should contain failure indicator"
51+
)
52+
XCTAssertTrue(
53+
error.message.contains("No such file or directory"),
54+
"Error message should include context about missing file"
55+
)
56+
}
57+
58+
func testPhaseScriptExecutionWithUnityGameAssembly() {
59+
let output = """
60+
/bin/sh -c /Users/evgeniyasenchurova/Library/Developer/Xcode/DerivedData/Unity-iPhone-gtnilxmbqexxvtcauewfdmpfbvfe/Build/Intermediates.noindex/ArchiveIntermediates/Unity-iPhone/IntermediateBuildFilesPath/Unity-iPhone.build/Release-iphoneos/GameAssembly.build/Script-C62A2A42F32E085EF849CF0B.sh
61+
/Users/evgeniyasenchurova/Library/Developer/Xcode/DerivedData/Unity-iPhone-gtnilxmbqexxvtcauewfdmpfbvfe/Build/Intermediates.noindex/ArchiveIntermediates/Unity-iPhone/IntermediateBuildFilesPath/Unity-iPhone.build/Release-iphoneos/GameAssembly.build/Script-C62A2A42F32E085EF849CF0B.sh: line 19: /Users/evgeniyasenchurova Downloads/ build_ios/Il2Cpp0utputProject/IL2CPP/build/deploy_arm64/il2cpp: Operation not permitted
62+
Command PhaseScriptExecution failed with a nonzero exit code
63+
"""
64+
65+
let result = parser.parse(input: output)
66+
67+
XCTAssertEqual(result.summary.errors, 1, "Should detect PhaseScriptExecution failure for Unity")
68+
XCTAssertFalse(result.errors.isEmpty, "Should have at least one error")
69+
70+
let error = result.errors[0]
71+
XCTAssertTrue(
72+
error.message.contains("Command PhaseScriptExecution failed"),
73+
"Error message should contain failure indicator"
74+
)
75+
XCTAssertTrue(
76+
error.message.contains("Operation not permitted"),
77+
"Error message should include operation permission error"
78+
)
79+
}
80+
81+
func testPhaseScriptExecutionWithMultipleErrors() {
82+
let output = """
83+
Build started...
84+
85+
Compiling Swift files...
86+
file.swift:10: error: Cannot find 'someFunction' in scope
87+
88+
Running post-build script...
89+
/bin/sh -c /path/to/script.sh
90+
Script execution failed
91+
Command PhaseScriptExecution failed with a nonzero exit code
92+
93+
Build complete!
94+
"""
95+
96+
let result = parser.parse(input: output)
97+
98+
// Should detect both the compilation error and the PhaseScriptExecution failure
99+
XCTAssertEqual(
100+
result.summary.errors,
101+
2,
102+
"Should detect both compilation error and PhaseScriptExecution failure"
103+
)
104+
105+
// Find the PhaseScriptExecution error
106+
let phaseError = result.errors.first { $0.message.contains("Command PhaseScriptExecution failed") }
107+
XCTAssertNotNil(phaseError, "Should have PhaseScriptExecution error")
108+
109+
if let phaseError = phaseError {
110+
XCTAssertTrue(
111+
phaseError.message.contains("Script execution failed"),
112+
"Error message should include preceding context"
113+
)
114+
}
115+
}
116+
117+
func testPhaseScriptExecutionWithSingleLineContext() {
118+
let output = """
119+
Running build phase script...
120+
Command PhaseScriptExecution failed with a nonzero exit code
121+
"""
122+
123+
let result = parser.parse(input: output)
124+
125+
XCTAssertEqual(result.summary.errors, 1, "Should detect PhaseScriptExecution failure with single context line")
126+
127+
let error = result.errors[0]
128+
XCTAssertTrue(
129+
error.message.contains("Command PhaseScriptExecution failed"),
130+
"Error message should contain failure indicator"
131+
)
132+
XCTAssertTrue(
133+
error.message.contains("Running build phase script"),
134+
"Error message should include context line"
135+
)
136+
}
137+
138+
func testPhaseScriptExecutionWithNoContext() {
139+
let output = """
140+
Command PhaseScriptExecution failed with a nonzero exit code
141+
"""
142+
143+
let result = parser.parse(input: output)
144+
145+
XCTAssertEqual(result.summary.errors, 1, "Should detect PhaseScriptExecution failure even with no context")
146+
147+
let error = result.errors[0]
148+
XCTAssertTrue(
149+
error.message.contains("Command PhaseScriptExecution failed"),
150+
"Error message should contain failure indicator"
151+
)
152+
}
153+
154+
func testPhaseScriptExecutionDoesNotDuplicateErrors() {
155+
let output = """
156+
/bin/sh -c /path/to/script.sh
157+
Command PhaseScriptExecution failed with a nonzero exit code
158+
/bin/sh -c /path/to/script.sh
159+
Command PhaseScriptExecution failed with a nonzero exit code
160+
"""
161+
162+
let result = parser.parse(input: output)
163+
164+
// Should deduplicate identical errors
165+
XCTAssertLessThanOrEqual(
166+
result.summary.errors,
167+
2,
168+
"Should not duplicate identical PhaseScriptExecution errors"
169+
)
170+
}
171+
172+
func testBuildSucceededDoesNotCreatePhaseError() {
173+
let output = """
174+
Running phase script...
175+
Build succeeded in 5.234 seconds
176+
"""
177+
178+
let result = parser.parse(input: output)
179+
180+
XCTAssertEqual(result.summary.errors, 0, "Build succeeded should not create a PhaseScriptExecution error")
181+
}
182+
183+
func testPhaseScriptExecutionWithComplexOutput() {
184+
let output = """
185+
** BUILD START **
186+
187+
Linking Framework/Module
188+
189+
Running script phase [CP] Copy Pods Resources
190+
191+
Resources copied...
192+
warning: Some resources were skipped
193+
194+
Running script phase custom build script
195+
196+
Processing configuration...
197+
/bin/sh -c /path/to/complex/script.sh
198+
Error: Configuration file not found at /expected/path
199+
Command PhaseScriptExecution failed with a nonzero exit code
200+
201+
Build failed after 10.234 seconds
202+
"""
203+
204+
let result = parser.parse(input: output)
205+
206+
// Should detect the PhaseScriptExecution failure
207+
let phaseError = result.errors.first { $0.message.contains("Command PhaseScriptExecution failed") }
208+
XCTAssertNotNil(phaseError, "Should detect PhaseScriptExecution failure in complex output")
209+
210+
if let phaseError = phaseError {
211+
XCTAssertTrue(
212+
phaseError.message.contains("Error: Configuration file not found"),
213+
"Should include relevant context from preceding lines"
214+
)
215+
}
216+
}
217+
218+
func testPhaseScriptExecutionFiltersUnrelatedWarnings() {
219+
// Test case based on user's real project scenario
220+
let output = """
221+
Warning: unknown environment variable SWIFT_DEBUG_INFORMATION_FORMAT
222+
bash: /Users/roman/Developer/SpaceTime/build_id.sh: No such file or directory
223+
Command PhaseScriptExecution failed with a nonzero exit code
224+
"""
225+
226+
let result = parser.parse(input: output)
227+
228+
XCTAssertEqual(result.summary.errors, 1, "Should detect PhaseScriptExecution failure")
229+
XCTAssertFalse(result.errors.isEmpty, "Should have at least one error")
230+
231+
let error = result.errors[0]
232+
XCTAssertTrue(
233+
error.message.contains("Command PhaseScriptExecution failed"),
234+
"Error message should contain failure indicator"
235+
)
236+
XCTAssertTrue(
237+
error.message.contains("bash:"),
238+
"Error message should include bash error context"
239+
)
240+
XCTAssertTrue(
241+
error.message.contains("No such file or directory"),
242+
"Error message should include error details"
243+
)
244+
XCTAssertFalse(
245+
error.message.contains("Warning: unknown environment variable"),
246+
"Should filter out unrelated Warning: lines"
247+
)
248+
}
249+
}

0 commit comments

Comments
 (0)