Skip to content

Commit

Permalink
[BUG] xcparse doesn't export screenshots for iPhone Xʀ on Xcode 11.1 …
Browse files Browse the repository at this point in the history
…or below (#31)

Change Description: These changes are to help address issues with using xcparse on Xcode 11.1 and below. Apple's xcresulttool, which we use to extract the screenshots, crashes in some versions when attempting to export out attachments to directory paths that have Unicode characters like "iPhone Xʀ" or "한국어". This leads to the user being unable to get their screenshots & not knowing why.

The changes here introduce the ability for xcparse to understand the version of xcresulttool & change behavior of exporting based off the versioning. For xcresulttool versions below 15500, we change "iPhone Xʀ" to "iPhone XR" & any other non-ASCII compatible characters into their lossy ASCII conversion (often "?"). For xcresulttool 15500 or above, we continue exporting how we always have. In all cases for users with xcresulttool below 15500, we warn them about the issue in their version and encourage them to update Xcode. In cases where users with xcresulttool version below 155000 have a destination folder path that can not be represented in ASCII, we hard-fail export and alert the user they need to update Xcode.

Test Plan/Testing Performed: Tested that with these changes, iPhone XR screenshots can be retrieved on Xcode 11.1. When using test run configuration names with Korean characters, confirmed that we export into a lossy ASCII version of the string (though this will lead to loss of some folders since "한국어" & "중국어" will both become "???")
  • Loading branch information
abotkin-cpi authored Nov 11, 2019
1 parent dcc689f commit 9d0c482
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 7 deletions.
47 changes: 47 additions & 0 deletions Sources/XCParseCore/Version+XCPTooling.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Version+XCPTooling.swift
// XCParseCore
//
// Created by Alex Botkin on 11/8/19.
//

import Foundation
import SPMUtility

public extension Version {
static func xcresulttoolCompatibleWithUnicodeExportPath() -> Version {
return Version(15500, 0, 0)
}

static func xcresulttool() -> Version? {
guard let xcresulttoolVersionResult = XCResultToolCommand.Version().run() else {
return nil
}
do {
let xcresultVersionString = try xcresulttoolVersionResult.utf8Output()

let components = xcresultVersionString.components(separatedBy: CharacterSet(charactersIn: ",\n"))
for string in components {
let trimmedString = string.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedString.hasPrefix("xcresulttool version ") {
let xcresulttoolVersionString = trimmedString.replacingOccurrences(of: "xcresulttool version ", with: "")
// Check to see if we can convert it to a number
var xcresulttoolVersion: Version?

if let xcresulttoolVersionInt = Int(xcresulttoolVersionString) {
xcresulttoolVersion = Version(xcresulttoolVersionInt, 0, 0)
} else {
xcresulttoolVersion = Version(string: xcresulttoolVersionString)
}

return xcresulttoolVersion
}
}

return nil
} catch {
print("Failed to parse xcresulttool version with error: \(error)")
return nil
}
}
}
12 changes: 12 additions & 0 deletions Sources/XCParseCore/XCResultToolCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,16 @@ open class XCResultToolCommand {
super.init(withXCResult: xcresult, process: process)
}
}

open class Version: XCResultToolCommand {

public init() {
var processArgs = xcresultToolArguments
processArgs.append(contentsOf: ["version"])

let xcresult = XCResult(path: "")
let process = Basic.Process(arguments: processArgs)
super.init(withXCResult: xcresult, process: process)
}
}
}
2 changes: 2 additions & 0 deletions Sources/xcparse/AttachmentsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ struct AttachmentsCommand: Command {
options.activitySummaryFilter = { additionalActivityTypes.contains($0.activityType) }
}

options.xcresulttoolCompatability = xcpParser.checkXCResultToolCompatability(destination: outputPath.pathString)

// Now let's get extracting
try xcpParser.extractAttachments(xcresultPath: xcresultPath.pathString,
destination: outputPath.pathString,
Expand Down
7 changes: 6 additions & 1 deletion Sources/xcparse/CommandRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,20 @@ struct CommandRegistry {

do {
let xcpParser = XCPParser()

let destination = legacyScreenshotPaths[1].path.pathString
let xcresulttoolCompatability = xcpParser.checkXCResultToolCompatability(destination: destination)

let options = AttachmentExportOptions(addTestScreenshotsDirectory: true,
divideByTargetModel: false,
divideByTargetOS: false,
divideByTestRun: false,
xcresulttoolCompatability: xcresulttoolCompatability,
attachmentFilter: {
return UTTypeConformsTo($0.uniformTypeIdentifier as CFString, "public.image" as CFString)
})
try xcpParser.extractAttachments(xcresultPath: legacyScreenshotPaths[0].path.pathString,
destination: legacyScreenshotPaths[1].path.pathString,
destination: destination,
options: options)

return true
Expand Down
2 changes: 2 additions & 0 deletions Sources/xcparse/ScreenshotsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ struct ScreenshotsCommand: Command {
options.activitySummaryFilter = { additionalActivityTypes.contains($0.activityType) }
}

options.xcresulttoolCompatability = xcpParser.checkXCResultToolCompatability(destination: outputPath.pathString)

try xcpParser.extractAttachments(xcresultPath: xcresultPath.pathString,
destination: outputPath.pathString,
options: options)
Expand Down
1 change: 1 addition & 0 deletions Sources/xcparse/VersionCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Basic
import Foundation
import SPMUtility
import XCParseCore

struct VersionCommand: Command {
let command = "version"
Expand Down
85 changes: 79 additions & 6 deletions Sources/xcparse/XCPParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
import SPMUtility
import XCParseCore

let xcparseCurrentVersion = Version(1, 0, 0)
let xcparseCurrentVersion = Version(1, 0, 1)

extension Foundation.URL {
func fileExistsAsDirectory() -> Bool {
Expand Down Expand Up @@ -49,13 +49,33 @@ extension Foundation.URL {
}
}

extension String {
func lossyASCIIString() -> String? {
let string = self.precomposedStringWithCanonicalMapping
guard let lossyASCIIData = string.data(using: .ascii, allowLossyConversion: true) else {
return nil
}
guard let lossyASCIIString = String(data: lossyASCIIData, encoding: .ascii) else {
return nil
}
return lossyASCIIString
}
}

struct XCResultToolCompatability {
var supportsExport: Bool = true
var supportsUnicodeExportPaths: Bool = true // See https://github.com/ChargePoint/xcparse/issues/30
}

struct AttachmentExportOptions {
var addTestScreenshotsDirectory: Bool = false
var divideByTargetModel: Bool = false
var divideByTargetOS: Bool = false
var divideByTestRun: Bool = false
var divideByTest: Bool = false

var xcresulttoolCompatability = XCResultToolCompatability()

var testSummaryFilter: (ActionTestSummary) -> Bool = { _ in
return true
}
Expand All @@ -78,16 +98,27 @@ struct AttachmentExportOptions {
func screenshotDirectoryURL(_ deviceRecord: ActionDeviceRecord, forBaseURL baseURL: Foundation.URL) -> Foundation.URL {
var targetDeviceFolderName: String? = nil

var modelName = deviceRecord.modelName
if self.xcresulttoolCompatability.supportsUnicodeExportPaths != true, modelName == "iPhone Xʀ" {
// For explaination, see https://github.com/ChargePoint/xcparse/issues/30
modelName = "iPhone XR"
}

if self.divideByTargetModel == true, self.divideByTargetOS == true {
targetDeviceFolderName = deviceRecord.modelName + " (\(deviceRecord.operatingSystemVersion))"
targetDeviceFolderName = modelName + " (\(deviceRecord.operatingSystemVersion))"
} else if self.divideByTargetModel {
targetDeviceFolderName = deviceRecord.modelName
targetDeviceFolderName = modelName
} else if self.divideByTargetOS {
targetDeviceFolderName = deviceRecord.operatingSystemVersion
}

if let folderName = targetDeviceFolderName {
return baseURL.appendingPathComponent(folderName, isDirectory: true)
if self.xcresulttoolCompatability.supportsUnicodeExportPaths != true {
let asciiFolderName = folderName.lossyASCIIString() ?? folderName
return baseURL.appendingPathComponent(asciiFolderName, isDirectory: true)
} else {
return baseURL.appendingPathComponent(folderName, isDirectory: true)
}
} else {
return baseURL
}
Expand All @@ -99,7 +130,12 @@ struct AttachmentExportOptions {
}

if self.divideByTestRun {
return baseURL.appendingPathComponent(testPlanRunName, isDirectory: true)
if self.xcresulttoolCompatability.supportsUnicodeExportPaths != true {
let asciiTestPlanRunName = testPlanRunName.lossyASCIIString() ?? testPlanRunName
return baseURL.appendingPathComponent(asciiTestPlanRunName, isDirectory: true)
} else {
return baseURL.appendingPathComponent(testPlanRunName, isDirectory: true)
}
} else {
return baseURL
}
Expand All @@ -111,7 +147,12 @@ struct AttachmentExportOptions {
}

if self.divideByTest == true {
return baseURL.appendingPathComponent(summaryIdentifier, isDirectory: true)
if self.xcresulttoolCompatability.supportsUnicodeExportPaths != true {
let asciiSummaryIdentifier = summaryIdentifier.lossyASCIIString() ?? summaryIdentifier
return baseURL.appendingPathComponent(asciiSummaryIdentifier, isDirectory: true)
} else {
return baseURL.appendingPathComponent(summaryIdentifier, isDirectory: true)
}
} else {
return baseURL
}
Expand All @@ -127,7 +168,39 @@ class XCPParser {
// MARK: -
// MARK: Parsing Actions

func checkXCResultToolCompatability(destination: String) -> XCResultToolCompatability {
var compatability = XCResultToolCompatability()

guard let xcresulttoolVersion = Version.xcresulttool() else {
self.console.writeMessage("Warning: Could not determine xcresulttool version", to: .standard)
return compatability
}

let unicodeExport = Version.xcresulttoolCompatibleWithUnicodeExportPath()
if xcresulttoolVersion < unicodeExport {
// For explaination, see https://github.com/ChargePoint/xcparse/issues/30
let asciiDestinationPath = destination.lossyASCIIString() ?? destination
if asciiDestinationPath != destination {
self.console.writeMessage("\nYour xcresulttool version \(xcresulttoolVersion.major) does not fully support Unicode export directory paths. Upgrade to Xcode 11.2.1 (xcresulttool version \(unicodeExport.major)) in order to export to your non-ASCII destination path.\n", to: .standard)

compatability.supportsExport = false
compatability.supportsUnicodeExportPaths = false
} else {
self.console.writeMessage("\nYour xcresulttool version \(xcresulttoolVersion.major) does not fully support Unicode export directory paths. Upgrade to Xcode 11.2.1 (xcresulttool version \(unicodeExport.major)) or above if you use non-Latin characters in your test run configuration names, attachment file names, or file system folder names.\n", to: .standard)

compatability.supportsUnicodeExportPaths = false
}
}

return compatability
}

func extractAttachments(xcresultPath: String, destination: String, options: AttachmentExportOptions = AttachmentExportOptions()) throws {
// Check the xcresulttool version is compatible to export the request
if options.xcresulttoolCompatability.supportsExport != true {
return
}

var xcresult = XCResult(path: xcresultPath, console: self.console)
guard let invocationRecord = xcresult.invocationRecord else {
return
Expand Down
4 changes: 4 additions & 0 deletions xcparse.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
62525EAE2376350100472F82 /* Version+XCPTooling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62525EAD2376350100472F82 /* Version+XCPTooling.swift */; };
62CC363E23553EA0003C7B68 /* XCResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CC363D23553EA0003C7B68 /* XCResult.swift */; };
62CC36592357C110003C7B68 /* AttachmentsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CC36582357C110003C7B68 /* AttachmentsCommand.swift */; };
OBJ_179 /* Await.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_73 /* Await.swift */; };
Expand Down Expand Up @@ -327,6 +328,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
62525EAD2376350100472F82 /* Version+XCPTooling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+XCPTooling.swift"; sourceTree = "<group>"; };
62CC363D23553EA0003C7B68 /* XCResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCResult.swift; sourceTree = "<group>"; };
62CC36582357C110003C7B68 /* AttachmentsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsCommand.swift; sourceTree = "<group>"; };
OBJ_10 /* ActionDeviceRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionDeviceRecord.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -945,6 +947,7 @@
OBJ_52 /* TestFailureIssueSummary.swift */,
OBJ_53 /* TypeDefinition.swift */,
OBJ_54 /* XCPResultDecoding.swift */,
62525EAD2376350100472F82 /* Version+XCPTooling.swift */,
OBJ_55 /* XCResultToolCommand.swift */,
62CC363D23553EA0003C7B68 /* XCResult.swift */,
);
Expand Down Expand Up @@ -1267,6 +1270,7 @@
OBJ_295 /* ActionTestSummaryIdentifiableObject.swift in Sources */,
OBJ_296 /* ActionTestableSummary.swift in Sources */,
OBJ_297 /* ActionsInvocationMetadata.swift in Sources */,
62525EAE2376350100472F82 /* Version+XCPTooling.swift in Sources */,
OBJ_298 /* ActionsInvocationRecord.swift in Sources */,
OBJ_299 /* ActivityLogAnalyzerControlFlowStep.swift in Sources */,
OBJ_300 /* ActivityLogAnalyzerControlFlowStepEdge.swift in Sources */,
Expand Down

0 comments on commit 9d0c482

Please sign in to comment.