Repository: cpisciotta/xcbeautify (swift)

Issue:
[New Output] Support Copy Output
### What's the uncaptured output?

```
Copy /path/to/some/file.extension /path/to/some/other/file.extension (in target 'Target' from project 'Project')
    cd /path/to/some/project
    builtin-copy -exclude .DS_Store -exclude CVS -exclude .svn -exclude .git -exclude .hg -resolve-src-symlinks /some/path /some/other/path
```

### Suggested Regex

_No response_

### Suggested Output

_No response_

### Additional Context

_No response_

Pull request description:
## Changes

Add a `CaptureGroup` to parse and format the following example output:

```
Copy /path/to/some/file.extension /path/to/some/other/file.extension (in target 'Target' from project 'Project')
```

## Testing

- Unit tests passed
- Performance regression tests showed positive improvements
- No missed instances from example build log

Patch:
diff --git a/Sources/XcbeautifyLib/CaptureGroups.swift b/Sources/XcbeautifyLib/CaptureGroups.swift
index 946792da..96be7510 100644
--- a/Sources/XcbeautifyLib/CaptureGroups.swift
+++ b/Sources/XcbeautifyLib/CaptureGroups.swift
@@ -362,6 +362,29 @@ struct CompileStoryboardCaptureGroup: CompileFileCaptureGroup {
     }
 }
 
+struct CopyFilesCaptureGroup: CaptureGroup {
+    static let outputType: OutputType = .task
+
+    // ((?:\S|(?<=\\) )+) --> Match any non-whitespace character OR any escaped space (space in filename)
+    static let regex = Regex(pattern: #"^Copy ((?:\S|(?<=\\) )+) ((?:\S|(?<=\\) )+) \(in target '(.*)' from project '.*'\)$"#)
+
+    let firstFilePath: String
+    let firstFilename: String
+    let secondFilePath: String
+    let secondFilename: String
+    let target: String
+
+    init?(groups: [String]) {
+        assert(groups.count == 3)
+        guard let firstFilePath = groups[safe: 0], let secondFilePath = groups[safe: 1], let target = groups[safe: 2] else { return nil }
+        self.firstFilePath = firstFilePath
+        firstFilename = firstFilePath.lastPathComponent
+        self.secondFilePath = secondFilePath
+        secondFilename = secondFilePath.lastPathComponent
+        self.target = target
+    }
+}
+
 struct CopyHeaderCaptureGroup: CopyCaptureGroup {
     static let outputType: OutputType = .task
 
diff --git a/Sources/XcbeautifyLib/Formatter.swift b/Sources/XcbeautifyLib/Formatter.swift
index 3431055d..b79296b7 100644
--- a/Sources/XcbeautifyLib/Formatter.swift
+++ b/Sources/XcbeautifyLib/Formatter.swift
@@ -64,6 +64,8 @@ package struct Formatter {
             return renderer.formatCompileWarning(group: group)
         case let group as CompileXibCaptureGroup:
             return renderer.formatCompile(group: group)
+        case let group as CopyFilesCaptureGroup:
+            return renderer.formatCopyFiles(group: group)
         case let group as CopyHeaderCaptureGroup:
             return renderer.formatCopy(group: group)
         case let group as CopyPlistCaptureGroup:
diff --git a/Sources/XcbeautifyLib/Parser.swift b/Sources/XcbeautifyLib/Parser.swift
index 5447deba..46848fb2 100644
--- a/Sources/XcbeautifyLib/Parser.swift
+++ b/Sources/XcbeautifyLib/Parser.swift
@@ -19,6 +19,7 @@ package final class Parser {
         CompileCommandCaptureGroup.self,
         CompileXibCaptureGroup.self,
         CompileStoryboardCaptureGroup.self,
+        CopyFilesCaptureGroup.self,
         CopyHeaderCaptureGroup.self,
         CopyPlistCaptureGroup.self,
         CopyStringsCaptureGroup.self,
diff --git a/Sources/XcbeautifyLib/Renderers/OutputRendering.swift b/Sources/XcbeautifyLib/Renderers/OutputRendering.swift
index d1f39546..1b8bd6e6 100644
--- a/Sources/XcbeautifyLib/Renderers/OutputRendering.swift
+++ b/Sources/XcbeautifyLib/Renderers/OutputRendering.swift
@@ -21,6 +21,7 @@ protocol OutputRendering {
     func formatCompileError(group: CompileErrorCaptureGroup) -> String
     func formatCompileWarning(group: CompileWarningCaptureGroup) -> String
     func formatCopy(group: any CopyCaptureGroup) -> String
+    func formatCopyFiles(group: CopyFilesCaptureGroup) -> String
     func formatCoverageReport(group: GeneratedCoverageReportCaptureGroup) -> String
     func formatCursor(group: CursorCaptureGroup) -> String?
     func formatDuplicateLocalizedStringKey(group: DuplicateLocalizedStringKeyCaptureGroup) -> String
@@ -134,6 +135,13 @@ extension OutputRendering {
         return colored ? "[\(target.f.Cyan)] \("Copying".s.Bold) \(filename)" : "[\(target)] Copying \(filename)"
     }
 
+    func formatCopyFiles(group: CopyFilesCaptureGroup) -> String {
+        let target = group.target
+        let firstFilename = group.firstFilename
+        let secondFilename = group.secondFilename
+        return colored ? "[\(target.f.Cyan)] \("Copy".s.Bold) \(firstFilename) -> \(secondFilename)" : "[\(target)] Copy \(firstFilename) -> \(secondFilename)"
+    }
+
     func formatCursor(group: CursorCaptureGroup) -> String? {
         nil
     }


Test patch:
diff --git a/Tests/XcbeautifyLibTests/ParserTests.swift b/Tests/XcbeautifyLibTests/ParserTests.swift
new file mode 100644
index 00000000..fc25fdd1
--- /dev/null
+++ b/Tests/XcbeautifyLibTests/ParserTests.swift
@@ -0,0 +1,43 @@
+//
+//  ParserTests.swift
+//
+//
+//  Created by Charles Pisciotta on 8/6/24.
+//
+
+import XCTest
+@testable import XcbeautifyLib
+
+final class ParserTests: XCTestCase {
+    private var parser: Parser!
+
+    override func setUpWithError() throws {
+        try super.setUpWithError()
+        parser = Parser()
+    }
+
+    override func tearDownWithError() throws {
+        parser = nil
+        try super.tearDownWithError()
+    }
+
+    func testMatchCopyFilesMatchingSourceAndDestinationFilenames() throws {
+        let input = #"Copy /path/to/some/file.swift /path/to/some/other/file.swift (in target 'Target' from project 'Project')"#
+        let captureGroup = try XCTUnwrap(parser.parse(line: input) as? CopyFilesCaptureGroup)
+        XCTAssertEqual(captureGroup.firstFilePath, "/path/to/some/file.swift")
+        XCTAssertEqual(captureGroup.firstFilename, "file.swift")
+        XCTAssertEqual(captureGroup.secondFilePath, "/path/to/some/other/file.swift")
+        XCTAssertEqual(captureGroup.secondFilename, "file.swift")
+        XCTAssertEqual(captureGroup.target, "Target")
+    }
+
+    func testMatchCopyFilesDifferentSourceAndDestinationFilenames() throws {
+        let input = #"Copy /Backyard-Birds/Build/Products/Debug/Backyard_Birds.swiftmodule/x86_64-apple-macos.abi.json /Backyard-Birds/Build/Intermediates.noindex/Backyard\ Birds.build/Debug/Backyard\ Birds.build/Objects-normal/x86_64/Backyard_Birds.abi.json (in target 'Backyard Birds' from project 'Backyard Birds')"#
+        let captureGroup = try XCTUnwrap(parser.parse(line: input) as? CopyFilesCaptureGroup)
+        XCTAssertEqual(captureGroup.firstFilePath, "/Backyard-Birds/Build/Products/Debug/Backyard_Birds.swiftmodule/x86_64-apple-macos.abi.json")
+        XCTAssertEqual(captureGroup.firstFilename, "x86_64-apple-macos.abi.json")
+        XCTAssertEqual(captureGroup.secondFilePath, #"/Backyard-Birds/Build/Intermediates.noindex/Backyard\ Birds.build/Debug/Backyard\ Birds.build/Objects-normal/x86_64/Backyard_Birds.abi.json"#)
+        XCTAssertEqual(captureGroup.secondFilename, "Backyard_Birds.abi.json")
+        XCTAssertEqual(captureGroup.target, "Backyard Birds")
+    }
+}
diff --git a/Tests/XcbeautifyLibTests/ParsingTests/ParsingTests.swift b/Tests/XcbeautifyLibTests/ParsingTests/ParsingTests.swift
index 2b17c091..6b00460d 100644
--- a/Tests/XcbeautifyLibTests/ParsingTests/ParsingTests.swift
+++ b/Tests/XcbeautifyLibTests/ParsingTests/ParsingTests.swift
@@ -26,9 +26,9 @@ final class ParsingTests: XCTestCase {
         // Update this magic number whenever `uncapturedOutput` is less than the current magic number.
         // There's a regression whenever `uncapturedOutput` is greater than the current magic number.
         #if os(macOS)
-        XCTAssertEqual(uncapturedOutput, 437)
+        XCTAssertEqual(uncapturedOutput, 381)
         #else
-        XCTAssertEqual(uncapturedOutput, 453)
+        XCTAssertEqual(uncapturedOutput, 397)
         #endif
     }
 
@@ -56,9 +56,9 @@ final class ParsingTests: XCTestCase {
         // Update this magic number whenever `uncapturedOutput` is less than the current magic number.
         // There's a regression whenever `uncapturedOutput` is greater than the current magic number.
         #if os(macOS)
-        XCTAssertEqual(uncapturedOutput, 14922)
+        XCTAssertEqual(uncapturedOutput, 12938)
         #else
-        XCTAssertEqual(uncapturedOutput, 15490)
+        XCTAssertEqual(uncapturedOutput, 13506)
         #endif
     }
 }
diff --git a/Tests/XcbeautifyLibTests/RendererTests/GitHubActionsRendererTests.swift b/Tests/XcbeautifyLibTests/RendererTests/GitHubActionsRendererTests.swift
index 4d30b1d2..a16c540e 100644
--- a/Tests/XcbeautifyLibTests/RendererTests/GitHubActionsRendererTests.swift
+++ b/Tests/XcbeautifyLibTests/RendererTests/GitHubActionsRendererTests.swift
@@ -164,6 +164,18 @@ final class GitHubActionsRendererTests: XCTestCase {
         XCTAssertEqual(logFormatted(input), output)
     }
 
+    func testCopyMatchingSourceAndDestinationFiles() {
+        let input = "Copy /path/to/some/file.swift /path/to/some/other/file.swift (in target 'Target' from project 'Project')"
+        let output = "[Target] Copy file.swift -> file.swift"
+        XCTAssertEqual(logFormatted(input), output)
+    }
+
+    func testCopyDifferentSourceAndDestinationFiles() {
+        let input = #"Copy /Backyard-Birds/Build/Products/Debug/Backyard_Birds.swiftmodule/x86_64-apple-macos.abi.json /Backyard-Birds/Build/Intermediates.noindex/Backyard\ Birds.build/Debug/Backyard\ Birds.build/Objects-normal/x86_64/Backyard_Birds.abi.json (in target 'Backyard Birds' from project 'Backyard Birds')"#
+        let output = "[Backyard Birds] Copy x86_64-apple-macos.abi.json -> Backyard_Birds.abi.json"
+        XCTAssertEqual(logFormatted(input), output)
+    }
+
     func testCursor() { }
 
     func testExecuted() throws {
diff --git a/Tests/XcbeautifyLibTests/RendererTests/TeamCityRendererTests.swift b/Tests/XcbeautifyLibTests/RendererTests/TeamCityRendererTests.swift
index 732579f3..c48588e7 100644
--- a/Tests/XcbeautifyLibTests/RendererTests/TeamCityRendererTests.swift
+++ b/Tests/XcbeautifyLibTests/RendererTests/TeamCityRendererTests.swift
@@ -167,6 +167,18 @@ final class TeamCityRendererTests: XCTestCase {
         XCTAssertEqual(noColoredFormatted(input), output)
     }
 
+    func testCopyMatchingSourceAndDestinationFiles() {
+        let input = "Copy /path/to/some/file.swift /path/to/some/other/file.swift (in target 'Target' from project 'Project')"
+        let output = "[Target] Copy file.swift -> file.swift"
+        XCTAssertEqual(noColoredFormatted(input), output)
+    }
+
+    func testCopyDifferentSourceAndDestinationFiles() {
+        let input = #"Copy /Backyard-Birds/Build/Products/Debug/Backyard_Birds.swiftmodule/x86_64-apple-macos.abi.json /Backyard-Birds/Build/Intermediates.noindex/Backyard\ Birds.build/Debug/Backyard\ Birds.build/Objects-normal/x86_64/Backyard_Birds.abi.json (in target 'Backyard Birds' from project 'Backyard Birds')"#
+        let output = "[Backyard Birds] Copy x86_64-apple-macos.abi.json -> Backyard_Birds.abi.json"
+        XCTAssertEqual(noColoredFormatted(input), output)
+    }
+
     func testCursor() { }
 
     func testExecutedWithoutSkipped() throws {
diff --git a/Tests/XcbeautifyLibTests/RendererTests/TerminalRendererTests.swift b/Tests/XcbeautifyLibTests/RendererTests/TerminalRendererTests.swift
index 33e8be1a..bc3ec2d9 100644
--- a/Tests/XcbeautifyLibTests/RendererTests/TerminalRendererTests.swift
+++ b/Tests/XcbeautifyLibTests/RendererTests/TerminalRendererTests.swift
@@ -164,6 +164,18 @@ final class TerminalRendererTests: XCTestCase {
         XCTAssertEqual(noColoredFormatted(input), output)
     }
 
+    func testCopyMatchingSourceAndDestinationFiles() {
+        let input = "Copy /path/to/some/file.swift /path/to/some/other/file.swift (in target 'Target' from project 'Project')"
+        let output = "[Target] Copy file.swift -> file.swift"
+        XCTAssertEqual(noColoredFormatted(input), output)
+    }
+
+    func testCopyDifferentSourceAndDestinationFiles() {
+        let input = #"Copy /Backyard-Birds/Build/Products/Debug/Backyard_Birds.swiftmodule/x86_64-apple-macos.abi.json /Backyard-Birds/Build/Intermediates.noindex/Backyard\ Birds.build/Debug/Backyard\ Birds.build/Objects-normal/x86_64/Backyard_Birds.abi.json (in target 'Backyard Birds' from project 'Backyard Birds')"#
+        let output = "[Backyard Birds] Copy x86_64-apple-macos.abi.json -> Backyard_Birds.abi.json"
+        XCTAssertEqual(noColoredFormatted(input), output)
+    }
+
     func testCursor() { }
 
     func testExecutedWithoutSkipped() throws {