Improve error feedback and prevent duplicate entries#44
Conversation
- Extract LicenseScanner struct for testable scanning logic - Track skipped files and print summary (verbose details behind #if DEV) - Break after first LICENSE found per package to prevent duplicates - Show acknowledgement count in success message - Add 7 LicenseScannerTests covering: file discovery, LICENSE variants, hidden dirs, duplicate prevention, invalid UTF-8 tracking, empty dirs
3b61e58 to
cfec7f7
Compare
There was a problem hiding this comment.
Pull request overview
Improves AckGen’s CLI build-phase output by extracting license scanning into a dedicated helper, summarizing skipped files, and preventing duplicate acknowledgements when multiple LICENSE variants exist in a package.
Changes:
- Added
LicenseScannerwith structured scan results (acknowledgements + skipped file metadata) and updated CLI output to summarize encoding skips. - Prevented duplicate entries by breaking after the first matching LICENSE variant per package.
- Added
DEVSwift compilation condition (debug-only) and introduced new unit tests for license scanning behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| Sources/AckGenCLI/AckGen.swift | Introduces LicenseScanner, summarizes skipped-file warnings, and updates success output to include generated count. |
| Tests/AckGenTests/LicenseScannerTests.swift | Adds test coverage for license scanning scenarios (variants, hidden dirs, duplicates, invalid UTF-8). |
| Package.swift | Defines DEV for debug builds of AckGenCLI and updates test target dependencies to include AckGenCLI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static let licenseFiles = ["LICENSE", "LICENSE.txt", "LICENSE.md"] | ||
|
|
||
| static func scan(checkoutsPath: String, fileManager: FileManager = .default) throws -> ScanResult { | ||
| let packageDirectories = try fileManager.contentsOfDirectory(atPath: checkoutsPath) |
There was a problem hiding this comment.
contentsOfDirectory(atPath:) returns entries in an unspecified order, so the generated acknowledgements array (and resulting plist) can be nondeterministic between runs/filesystems. Consider sorting packageDirectories (or result.acknowledgements via .sorted()) before encoding to keep output stable and reduce noisy diffs in build phases.
| let packageDirectories = try fileManager.contentsOfDirectory(atPath: checkoutsPath) | |
| let packageDirectories = try fileManager.contentsOfDirectory(atPath: checkoutsPath).sorted() |
| struct ScanResult { | ||
| var acknowledgements: [Acknowledgement] | ||
| var skippedFiles: [(package: String, file: String, reason: String)] |
There was a problem hiding this comment.
skippedFiles is typed as an inline tuple here and the same tuple shape is repeated in printSkippedFilesSummary. To avoid the type drifting over time, consider introducing a dedicated SkippedFile struct (or a typealias) and reusing it in both places.
| struct ScanResult { | |
| var acknowledgements: [Acknowledgement] | |
| var skippedFiles: [(package: String, file: String, reason: String)] | |
| typealias SkippedFile = (package: String, file: String, reason: String) | |
| struct ScanResult { | |
| var acknowledgements: [Acknowledgement] | |
| var skippedFiles: [SkippedFile] |
| override func setUp() { | ||
| super.setUp() | ||
| tempDir = NSTemporaryDirectory() + "AckGenTests-\(UUID().uuidString)" | ||
| try! fman.createDirectory(atPath: tempDir, withIntermediateDirectories: true) | ||
| } | ||
|
|
||
| override func tearDown() { | ||
| try? fman.removeItem(atPath: tempDir) | ||
| super.tearDown() | ||
| } | ||
|
|
||
| // MARK: - Helpers | ||
|
|
||
| private func createPackage(_ name: String, licenseFile: String = "LICENSE", content: String = "MIT License") { | ||
| let pkgPath = "\(tempDir!)/\(name)" | ||
| try! fman.createDirectory(atPath: pkgPath, withIntermediateDirectories: true) | ||
| let data = content.data(using: .utf8)! |
There was a problem hiding this comment.
The test setup/helpers use try! and force unwraps (tempDir!, data(using:)!). If anything goes wrong (e.g. temp dir creation), this will crash the test process rather than reporting a test failure. Prefer setUpWithError() (or XCTUnwrap + XCTFail) and throwing helpers so failures are surfaced as XCTest failures instead of fatal errors.
| XCTAssertEqual(result.skippedFiles.first?.package, "BadEncoding") | ||
| XCTAssertEqual(result.skippedFiles.first?.reason, "invalid UTF-8 encoding") | ||
| } | ||
|
|
There was a problem hiding this comment.
LicenseScanner.scan has a behavior branch where the first matching license file is present but invalid UTF-8, and a later variant (e.g. LICENSE.md) is valid; this would currently record a skipped file and still generate an acknowledgement. There isn’t a test covering that mixed case—adding one would lock in the intended behavior for skippedFiles reporting.
| func testScanRecordsSkippedInvalidFirstLicenseButUsesValidVariant() throws { | |
| // Package has an invalid UTF-8 LICENSE but a valid LICENSE.md. | |
| // Scanner should record the invalid file in skippedFiles and still | |
| // produce an acknowledgement based on the valid variant. | |
| let pkgPath = "\(tempDir!)/MixedLicense" | |
| try fman.createDirectory(atPath: pkgPath, withIntermediateDirectories: true) | |
| // Create an invalid UTF-8 LICENSE file. | |
| let invalidData = Data([0xFF]) | |
| fman.createFile(atPath: "\(pkgPath)/LICENSE", contents: invalidData) | |
| // And a valid UTF-8 LICENSE.md variant. | |
| fman.createFile(atPath: "\(pkgPath)/LICENSE.md", contents: "MIT License".data(using: .utf8)!) | |
| let result = try LicenseScanner.scan(checkoutsPath: tempDir) | |
| XCTAssertEqual(result.acknowledgements.count, 1) | |
| XCTAssertEqual(result.acknowledgements.first?.title, "MixedLicense") | |
| XCTAssertEqual(result.skippedFiles.count, 1) | |
| XCTAssertEqual(result.skippedFiles.first?.package, "MixedLicense") | |
| XCTAssertEqual(result.skippedFiles.first?.reason, "invalid UTF-8 encoding") | |
| } |
Reflect new LicenseScanner struct, DEV compiler flag, unused EnvironmentKey annotation, and new LicenseScannerTests.
Summary
Improves CLI output quality for Xcode build phase usage.
Changes
Error Tracking
#if DEV)DEVcompilation condition defined for debug config inPackage.swiftDuplicate Prevention
LICENSEandLICENSE.mdexist)Success Message
"✓ Generated 12 acknowledgement(s) at: path"Testing
Stacked On
This PR is stacked on #32 → #31