diff --git a/Package.swift b/Package.swift index 539d812..ecd215e 100644 --- a/Package.swift +++ b/Package.swift @@ -5,9 +5,6 @@ import PackageDescription let package = Package( name: "xcparse", - platforms: [ - .macOS(.v10_13), - ], products: [ .executable(name: "xcparse", targets: ["xcparse"]), .library( diff --git a/Sources/XCParseCore/UTICompat.swift b/Sources/XCParseCore/UTICompat.swift new file mode 100644 index 0000000..f05c6f2 --- /dev/null +++ b/Sources/XCParseCore/UTICompat.swift @@ -0,0 +1,41 @@ +// +// UTICompat.swift +// XCParseCore +// +// Created for Linux compilation support. +// + +import Foundation +#if canImport(CoreServices) +import CoreServices +#endif + +public func utiConforms(_ uti: String, to parentUTI: String) -> Bool { + #if canImport(CoreServices) + return UTTypeConformsTo(uti as CFString, parentUTI as CFString) + #else + if uti == parentUTI { return true } + + let hierarchy: [String: [String]] = [ + "public.data": ["public.image", "public.text", "public.json", "public.xml"], + "public.image": [ + "public.jpeg", "public.png", "public.heic", "public.heif", + "public.tiff", "com.compuserve.gif", "public.svg-image", + "com.apple.icns", "com.microsoft.bmp", + ], + "public.text": ["public.plain-text", "public.html", "public.json", "public.xml"], + "public.plain-text": ["public.utf8-plain-text", "public.utf16-plain-text"], + "public.json": [], + "public.xml": [], + ] + + if let children = hierarchy[parentUTI] { + if children.contains(uti) { return true } + for child in children { + if utiConforms(uti, to: child) { return true } + } + } + + return false + #endif +} diff --git a/Sources/xcparse/AttachmentsCommand.swift b/Sources/xcparse/AttachmentsCommand.swift index 05c2f88..e14f83e 100644 --- a/Sources/xcparse/AttachmentsCommand.swift +++ b/Sources/xcparse/AttachmentsCommand.swift @@ -9,6 +9,7 @@ import Foundation import TSCBasic import TSCUtility +import XCParseCore struct AttachmentsCommand: Command { let command = "attachments" @@ -88,9 +89,9 @@ struct AttachmentsCommand: Command { divideByTest: arguments.get(self.divideByTest) ?? false) if let allowedUTIsToExport = arguments.get(self.utiWhitelist) { options.attachmentFilter = { - let attachmentUTI = $0.uniformTypeIdentifier as CFString + let attachmentUTI = $0.uniformTypeIdentifier for allowedUTI in allowedUTIsToExport { - if UTTypeConformsTo(attachmentUTI, allowedUTI as CFString) { + if utiConforms(attachmentUTI, to: allowedUTI) { return true } } diff --git a/Sources/xcparse/CommandRegistry.swift b/Sources/xcparse/CommandRegistry.swift index 75d1296..0ffcf9a 100644 --- a/Sources/xcparse/CommandRegistry.swift +++ b/Sources/xcparse/CommandRegistry.swift @@ -9,6 +9,7 @@ import Foundation import TSCBasic import TSCUtility +import XCParseCore // This is cribbed form a great blog post on ArgumentParser // https://www.enekoalonso.com/articles/handling-commands-with-swift-package-manager @@ -70,7 +71,7 @@ struct CommandRegistry { divideByTestPlanConfig: false, xcresulttoolCompatability: xcresulttoolCompatability, attachmentFilter: { - return UTTypeConformsTo($0.uniformTypeIdentifier as CFString, "public.image" as CFString) + return utiConforms($0.uniformTypeIdentifier, to: "public.image") }) try xcpParser.extractAttachments(xcresultPath: legacyScreenshotPaths[0].path.pathString, destination: destination, diff --git a/Sources/xcparse/ScreenshotsCommand.swift b/Sources/xcparse/ScreenshotsCommand.swift index 77af67a..2d2d2f5 100644 --- a/Sources/xcparse/ScreenshotsCommand.swift +++ b/Sources/xcparse/ScreenshotsCommand.swift @@ -9,6 +9,7 @@ import Foundation import TSCBasic import TSCUtility +import XCParseCore struct ScreenshotsCommand: Command { let command = "screenshots" @@ -89,7 +90,7 @@ struct ScreenshotsCommand: Command { divideByRegion: arguments.get(self.divideByRegion) ?? false, divideByTest: arguments.get(self.divideByTest) ?? false, attachmentFilter: { - return UTTypeConformsTo($0.uniformTypeIdentifier as CFString, "public.image" as CFString) + return utiConforms($0.uniformTypeIdentifier, to: "public.image") }) if let allowedTestStatuses = arguments.get(self.testStatusWhitelist) { options.testSummaryFilter = { allowedTestStatuses.contains($0.testStatus) } diff --git a/Sources/xcparse/XCPParser.swift b/Sources/xcparse/XCPParser.swift index da3ef0e..6ea00ee 100644 --- a/Sources/xcparse/XCPParser.swift +++ b/Sources/xcparse/XCPParser.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif import TSCBasic import TSCUtility import XCParseCore diff --git a/Tests/appThinningConverterTests/ReportConverterTests.swift b/Tests/appThinningConverterTests/ReportConverterTests.swift index c5f2944..2e4c4cf 100644 --- a/Tests/appThinningConverterTests/ReportConverterTests.swift +++ b/Tests/appThinningConverterTests/ReportConverterTests.swift @@ -16,8 +16,6 @@ final class ReportConverterTests: XCTestCase { } func testParseTextReturnsExpected() throws { - let bundle = Bundle(for: type(of: self)) - let filepath = try Resource(name: "App Thinning Size Report", type: "txt") let file = try String(contentsOfFile: filepath.url.path) let text = try XCTUnwrap(file) diff --git a/Tests/xcparseTests/ScreenshotCommandUTITests.swift b/Tests/xcparseTests/ScreenshotCommandUTITests.swift index ee6af49..53e158e 100644 --- a/Tests/xcparseTests/ScreenshotCommandUTITests.swift +++ b/Tests/xcparseTests/ScreenshotCommandUTITests.swift @@ -7,6 +7,7 @@ import XCTest import class Foundation.Bundle +import XCParseCore final class ScreenshotCommandUTITests: XCTestCase { func testScreenshotUTIFilter() throws { @@ -19,7 +20,7 @@ final class ScreenshotCommandUTITests: XCTestCase { ] for UTI in imageUTIs { - let conformsAsImage = UTTypeConformsTo(UTI as CFString, "public.image" as CFString) + let conformsAsImage = utiConforms(UTI, to: "public.image") XCTAssertTrue(conformsAsImage, "\(UTI) does not conform to public.image UTI. Screenshots command will not extract") } @@ -29,7 +30,7 @@ final class ScreenshotCommandUTITests: XCTestCase { ] for UTI in nonImageUTIs { - let conformsAsImage = UTTypeConformsTo(UTI as CFString, "public.image" as CFString) + let conformsAsImage = utiConforms(UTI, to: "public.image") XCTAssertFalse(conformsAsImage, "\(UTI) unexpectedly conforms to public.image UTI. Screenshots command will extract this") } } diff --git a/Tests/xcparseTests/UTICompatTests.swift b/Tests/xcparseTests/UTICompatTests.swift new file mode 100644 index 0000000..caf0041 --- /dev/null +++ b/Tests/xcparseTests/UTICompatTests.swift @@ -0,0 +1,122 @@ +// +// UTICompatTests.swift +// +// Tests for cross-platform UTI conformance checking. +// + +import XCTest +import XCParseCore + +final class UTICompatTests: XCTestCase { + + // MARK: - Identity + + func testIdentityConformance() { + XCTAssertTrue(utiConforms("public.image", to: "public.image")) + XCTAssertTrue(utiConforms("public.data", to: "public.data")) + XCTAssertTrue(utiConforms("public.text", to: "public.text")) + XCTAssertTrue(utiConforms("public.json", to: "public.json")) + } + + // MARK: - Image types + + func testImageConformance() { + let imageUTIs = [ + "public.heic", + "public.heif", + "public.png", + "public.jpeg", + "com.compuserve.gif", + "public.tiff", + "com.microsoft.bmp", + "public.svg-image", + "com.apple.icns", + ] + for uti in imageUTIs { + XCTAssertTrue(utiConforms(uti, to: "public.image"), + "\(uti) should conform to public.image") + } + } + + func testNonImageDoesNotConformToImage() { + let nonImageUTIs = [ + "public.plain-text", + "public.json", + "public.xml", + "com.adobe.pdf", + "public.html", + ] + for uti in nonImageUTIs { + XCTAssertFalse(utiConforms(uti, to: "public.image"), + "\(uti) should not conform to public.image") + } + } + + // MARK: - Text types + + func testPlainTextConformance() { + XCTAssertTrue(utiConforms("public.plain-text", to: "public.text")) + XCTAssertTrue(utiConforms("public.utf8-plain-text", to: "public.plain-text")) + XCTAssertTrue(utiConforms("public.utf16-plain-text", to: "public.plain-text")) + } + + func testTextSubtypes() { + XCTAssertTrue(utiConforms("public.html", to: "public.text")) + XCTAssertTrue(utiConforms("public.json", to: "public.text")) + XCTAssertTrue(utiConforms("public.xml", to: "public.text")) + } + + func testTransitivePlainTextToText() { + XCTAssertTrue(utiConforms("public.utf8-plain-text", to: "public.text")) + XCTAssertTrue(utiConforms("public.utf16-plain-text", to: "public.text")) + } + + // MARK: - Data (public.data) hierarchy + + func testDataConformance() { + XCTAssertTrue(utiConforms("public.image", to: "public.data")) + XCTAssertTrue(utiConforms("public.text", to: "public.data")) + XCTAssertTrue(utiConforms("public.json", to: "public.data")) + XCTAssertTrue(utiConforms("public.xml", to: "public.data")) + } + + func testTransitiveImageToData() { + XCTAssertTrue(utiConforms("public.png", to: "public.data")) + XCTAssertTrue(utiConforms("public.jpeg", to: "public.data")) + XCTAssertTrue(utiConforms("com.compuserve.gif", to: "public.data")) + } + + func testTransitiveTextToData() { + XCTAssertTrue(utiConforms("public.plain-text", to: "public.data")) + XCTAssertTrue(utiConforms("public.html", to: "public.data")) + } + + // MARK: - Negative cases + + func testUnknownUTI() { + XCTAssertFalse(utiConforms("com.example.unknown", to: "public.image")) + XCTAssertFalse(utiConforms("com.example.unknown", to: "public.data")) + } + + func testReverseConformanceDoesNotHold() { + XCTAssertFalse(utiConforms("public.image", to: "public.png")) + XCTAssertFalse(utiConforms("public.text", to: "public.plain-text")) + XCTAssertFalse(utiConforms("public.data", to: "public.image")) + } + + // MARK: - All tests (Linux) + + static var allTests = [ + ("testIdentityConformance", testIdentityConformance), + ("testImageConformance", testImageConformance), + ("testNonImageDoesNotConformToImage", testNonImageDoesNotConformToImage), + ("testPlainTextConformance", testPlainTextConformance), + ("testTextSubtypes", testTextSubtypes), + ("testTransitivePlainTextToText", testTransitivePlainTextToText), + ("testDataConformance", testDataConformance), + ("testTransitiveImageToData", testTransitiveImageToData), + ("testTransitiveTextToData", testTransitiveTextToData), + ("testUnknownUTI", testUnknownUTI), + ("testReverseConformanceDoesNotHold", testReverseConformanceDoesNotHold), + ] +} diff --git a/Tests/xcparseTests/XCTestManifests.swift b/Tests/xcparseTests/XCTestManifests.swift index fde0dac..4b68eaf 100644 --- a/Tests/xcparseTests/XCTestManifests.swift +++ b/Tests/xcparseTests/XCTestManifests.swift @@ -4,6 +4,8 @@ import XCTest public func allTests() -> [XCTestCaseEntry] { return [ testCase(xcparseTests.allTests), + testCase(ScreenshotCommandUTITests.allTests), + testCase(UTICompatTests.allTests), ] } #endif diff --git a/Tests/xcparseTests/xcparseTests.swift b/Tests/xcparseTests/xcparseTests.swift index 88aeefb..780f036 100644 --- a/Tests/xcparseTests/xcparseTests.swift +++ b/Tests/xcparseTests/xcparseTests.swift @@ -26,7 +26,7 @@ final class xcparseTests: XCTestCase { lazy var temporaryOutputDirectoryURL:URL = { // Setup a temp test folder that can be used as a sandbox let tempDirectoryURL = FileManager.default.temporaryDirectory - let temporaryOutputDirectoryName = ProcessInfo().globallyUniqueString + let temporaryOutputDirectoryName = ProcessInfo.processInfo.globallyUniqueString let temporaryOutputDirectoryURL = tempDirectoryURL.appendingPathComponent(temporaryOutputDirectoryName) return temporaryOutputDirectoryURL