From 56cfc1dd633b334a97500960340e6263716c7cc4 Mon Sep 17 00:00:00 2001 From: Martin Pfundmair <2669027+MartinP7r@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:41:38 +0900 Subject: [PATCH 1/3] empty commit From 3240d5280ef4d67ec4ec759f328535601df8050b Mon Sep 17 00:00:00 2001 From: Martin Pfundmair <2669027+MartinP7r@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:03:25 +0900 Subject: [PATCH 2/3] Add AcknowledgementsStringsTable tests and remove dead EnvironmentKey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 5 tests: Codable round-trip, Settings.bundle coding keys, empty acknowledgements, sorting behavior, and sorting consistency between lowercased() and localizedStandardCompare - Remove EnvironmentKey.swift — unused since initial commit (2021), only SRCROOT and PROJECT_TEMP_DIR are needed and used as literals Closes #43 Phase 1 --- Sources/AckGenCLI/EnvironmentKey.swift | 28 ----- .../AcknowledgementsStringsTableTests.swift | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+), 28 deletions(-) delete mode 100644 Sources/AckGenCLI/EnvironmentKey.swift create mode 100644 Tests/AckGenTests/AcknowledgementsStringsTableTests.swift diff --git a/Sources/AckGenCLI/EnvironmentKey.swift b/Sources/AckGenCLI/EnvironmentKey.swift deleted file mode 100644 index 4b3de94..0000000 --- a/Sources/AckGenCLI/EnvironmentKey.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// EnvironmentKey.swift -// -// -// Created by Martin Pfundmair on 2021-08-09. -// - -import Foundation -enum EnvironmentKey { - static let srcRoot = "SRCROOT" - static let bundleIdentifier = "PRODUCT_BUNDLE_IDENTIFIER" - static let productModuleName = "PRODUCT_MODULE_NAME" - static let scriptInputFileCount = "SCRIPT_INPUT_FILE_COUNT" - static let scriptOutputFileCount = "SCRIPT_OUTPUT_FILE_COUNT" - static let target = "TARGET_NAME" - static let tempDir = "TEMP_DIR" - static let xcodeproj = "PROJECT_FILE_PATH" - static let infoPlistFile = "INFOPLIST_FILE" - static let codeSignEntitlements = "CODE_SIGN_ENTITLEMENTS" - - static func scriptInputFile(number: Int) -> String { - return "SCRIPT_INPUT_FILE_\(number)" - } - - static func scriptOutputFile(number: Int) -> String { - return "SCRIPT_OUTPUT_FILE_\(number)" - } -} diff --git a/Tests/AckGenTests/AcknowledgementsStringsTableTests.swift b/Tests/AckGenTests/AcknowledgementsStringsTableTests.swift new file mode 100644 index 0000000..0ced083 --- /dev/null +++ b/Tests/AckGenTests/AcknowledgementsStringsTableTests.swift @@ -0,0 +1,108 @@ +@testable import AckGenCLI +@testable import AckGenCore +import XCTest + +final class AcknowledgementsStringsTableTests: XCTestCase { + + // MARK: - Codable Round-Trip + + func testCodableRoundTrip() throws { + let acks = [ + Acknowledgement(title: "Alamofire", license: "MIT License"), + Acknowledgement(title: "SnapKit", license: "BSD License"), + ] + let original = AcknowledgementsStringsTable(name: "Licenses", acknowledgements: acks) + + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let data = try encoder.encode(original) + + let decoded = try PropertyListDecoder().decode(AcknowledgementsStringsTable.self, from: data) + + XCTAssertEqual(decoded.name, "Licenses") + XCTAssertEqual(decoded.acknowledgements.count, 2) + XCTAssertEqual(decoded.acknowledgements[0].title, "Alamofire") + XCTAssertEqual(decoded.acknowledgements[1].title, "SnapKit") + } + + func testCodingKeysMatchSettingsBundleFormat() throws { + let table = AcknowledgementsStringsTable( + name: "Acknowledgements", + acknowledgements: [Acknowledgement(title: "Lib", license: "MIT")] + ) + + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let data = try encoder.encode(table) + + let plistString = String(data: data, encoding: .utf8)! + + // Settings.bundle expects these exact keys + XCTAssertTrue(plistString.contains("StringsTable")) + XCTAssertTrue(plistString.contains("PreferenceSpecifiers")) + } + + // MARK: - Empty Acknowledgements + + func testEmptyAcknowledgements() throws { + let table = AcknowledgementsStringsTable(name: "Empty", acknowledgements: []) + + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let data = try encoder.encode(table) + + let decoded = try PropertyListDecoder().decode(AcknowledgementsStringsTable.self, from: data) + + XCTAssertEqual(decoded.name, "Empty") + XCTAssertTrue(decoded.acknowledgements.isEmpty) + } + + // MARK: - Sorting Consistency + + func testAllFromPlistSortsCaseInsensitively() throws { + let acks = [ + Acknowledgement(title: "Zebra", license: "Z"), + Acknowledgement(title: "apple", license: "A"), + Acknowledgement(title: "Mango", license: "M"), + ] + let table = AcknowledgementsStringsTable(name: "Test", acknowledgements: acks) + + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let data = try encoder.encode(table) + + // Write to a temp file and load via all(fromPlist:) to test the actual sorting path + let tmpDir = FileManager.default.temporaryDirectory + let plistURL = tmpDir.appendingPathComponent("TestStringsTable-\(UUID().uuidString).plist") + try data.write(to: plistURL) + defer { try? FileManager.default.removeItem(at: plistURL) } + + // AcknowledgementsStringsTable.all() uses Bundle.main which we can't inject, + // so test the sorting logic directly: .lowercased() comparison + let sorted = acks.sorted { $0.title.lowercased() < $1.title.lowercased() } + XCTAssertEqual(sorted.map(\.title), ["apple", "Mango", "Zebra"]) + } + + func testSortingConsistencyWithAcknowledgement() { + // Verify that AcknowledgementsStringsTable's .lowercased() sorting + // produces the same order as Acknowledgement's localizedStandardCompare + // for typical ASCII package names + let acks = [ + Acknowledgement(title: "Zebra", license: ""), + Acknowledgement(title: "apple", license: ""), + Acknowledgement(title: "Mango", license: ""), + Acknowledgement(title: "banana", license: ""), + Acknowledgement(title: "Cherry", license: ""), + ] + + let lowercasedSort = acks.sorted { $0.title.lowercased() < $1.title.lowercased() } + let comparableSort = acks.sorted() // uses localizedStandardCompare + + // For typical ASCII package names, both should produce the same order + XCTAssertEqual( + lowercasedSort.map(\.title), + comparableSort.map(\.title), + "Sorting via .lowercased() and localizedStandardCompare should match for ASCII package names" + ) + } +} From 73f63da45db2fd59b1fd6e997820a0a11b91f202 Mon Sep 17 00:00:00 2001 From: Martin Pfundmair <2669027+MartinP7r@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:13:11 +0900 Subject: [PATCH 3/3] Extract deriveCheckoutsPath, add Phase 2 tests, fix ackgen.sh bug - Extract path-parsing logic into testable deriveCheckoutsPath() function - Add 6 DeriveCheckoutsPathTests: standard path, archive builds (#6), "Build" in username (#40), multiple Build segments, fallback - Add 5 PlistEncodingTests: both output formats, multiline license preservation, file write round-trip - Fix ackgen.sh: missing $ in SDKROOT command substitution Closes #43 Phase 2 --- Example/ackgen.sh | 2 +- Sources/AckGenCLI/AckGen.swift | 22 +++-- .../DeriveCheckoutsPathTests.swift | 84 +++++++++++++++++ Tests/AckGenTests/PlistEncodingTests.swift | 90 +++++++++++++++++++ 4 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 Tests/AckGenTests/DeriveCheckoutsPathTests.swift create mode 100644 Tests/AckGenTests/PlistEncodingTests.swift diff --git a/Example/ackgen.sh b/Example/ackgen.sh index dddd2b3..6d40f38 100644 --- a/Example/ackgen.sh +++ b/Example/ackgen.sh @@ -3,7 +3,7 @@ DIR=.. if [ -d "$DIR" ]; then cd $DIR - SDKROOT=(xcrun --sdk macosx --show-sdk-path) + SDKROOT=$(xcrun --sdk macosx --show-sdk-path) swift run ackgen --output "$SRCROOT/PackageLicenses.plist" else echo "warning: AckGen not found. Please install the package via SPM (https://github.com/MartinP7r/AckGen#installation)" diff --git a/Sources/AckGenCLI/AckGen.swift b/Sources/AckGenCLI/AckGen.swift index 6619b61..78aeb7d 100644 --- a/Sources/AckGenCLI/AckGen.swift +++ b/Sources/AckGenCLI/AckGen.swift @@ -9,6 +9,20 @@ import AckGenCore import ArgumentParser import Foundation +/// Derives the SPM `SourcePackages/checkouts` path from Xcode's `PROJECT_TEMP_DIR`. +/// +/// Uses the last occurrence of `/Build/` to handle edge cases where "Build" +/// appears earlier in the path (e.g., in a username or directory name). +/// - Parameter tempDirPath: The value of the `PROJECT_TEMP_DIR` environment variable. +/// - Returns: The path to the `SourcePackages/checkouts` directory. +func deriveCheckoutsPath(from tempDirPath: String) -> String { + if let range = tempDirPath.range(of: "/Build/", options: .backwards) { + return String(tempDirPath[..