diff --git a/README.md b/README.md index d23e2d5..1275f02 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Below are a few examples of common commands. For further assistance, use the --h ### Screenshots ``` -xcparse screenshots --os --model --test-run /path/to/Test.xcresult /path/to/exportScreenshots +xcparse screenshots --os --model --test-run /path/to/Test.xcresult /path/to/outputDirectory ``` This will cause screenshots to be exported like so: @@ -33,12 +33,24 @@ This will cause screenshots to be exported like so: Options can be added & remove to change the folder structure used for export. Using no options will lead to all attachments being exported into the output directory. +### Attachments + +``` +xcparse attachments /path/to/Test.xcresult /path/to/outputDirectory --uti public.plain-text public.image +``` + +Export all attachments in the xcresult that conform to either the ```public.plan-text``` or the ```public.image``` uniform type identifiers (UTI). The screenshots command, for example, is actually just the attachments command operating a whitelist for ```public.image``` UTI attachments. Other common types in xcresults are ```public.plain-text``` for debug descriptions of test failures. + +Read [this Apple documentation]((https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html#//apple_ref/doc/uid/TP40009259-SW1)) for a list of publicly documented UTIs. + ### Code Coverage ``` xcparse codecov /path/to/Test.xcresult /path/to/exportCodeCoverageFiles ``` +This will export the action.xccovreport & action.xccovarchive into your output directory. + ### Logs ``` @@ -53,6 +65,8 @@ xcparse --help xcparse screenshots --help ``` +Learn about all the options we didn't mention with ```--help```! + ## Modes ### Static Mode diff --git a/Sources/XCParseCore/ActionTestAttachment.swift b/Sources/XCParseCore/ActionTestAttachment.swift index cb5da9e..d69ac6b 100644 --- a/Sources/XCParseCore/ActionTestAttachment.swift +++ b/Sources/XCParseCore/ActionTestAttachment.swift @@ -9,13 +9,7 @@ import Foundation open class ActionTestAttachment : Codable { - enum UniformTypeIdentifier: String { - case jpeg = "public.jpeg" - case png = "public.png" - case txt = "public.plain-text" - } - - public let uniformTypeIdentifier: String + public let uniformTypeIdentifier: String // Note: You'll want to use CoreServices' UTType functions with this public let name: String? public let timestamp: Date? // public let userInfo: SortedKeyValueArray? diff --git a/Sources/xcparse/AttachmentsCommand.swift b/Sources/xcparse/AttachmentsCommand.swift new file mode 100644 index 0000000..26f64a6 --- /dev/null +++ b/Sources/xcparse/AttachmentsCommand.swift @@ -0,0 +1,86 @@ +// +// AttachmentsCommand.swift +// xcparse +// +// Created by Alex Botkin on 10/16/19. +// Copyright © 2019 ChargePoint, Inc. All rights reserved. +// + +import Basic +import Foundation +import SPMUtility + +struct AttachmentsCommand: Command { + let command = "attachments" + let overview = "Extracts attachments from xcresult and saves it in output folder." + let usage = "[OPTIONS] xcresult [outputDirectory]" + + var path: PositionalArgument + var outputPath: PositionalArgument + var verbose: OptionArgument + + var divideByModel: OptionArgument + var divideByOS: OptionArgument + var divideByTestPlanRun: OptionArgument + + var utiWhitelist: OptionArgument<[String]> + + init(parser: ArgumentParser) { + let subparser = parser.add(subparser: command, usage: usage, overview: overview) + path = subparser.add(positional: "xcresult", kind: PathArgument.self, + optional: false, usage: "Path to the xcresult file", completion: .filename) + outputPath = subparser.add(positional: "outputDirectory", kind: PathArgument.self, + optional: true, usage: "Folder to export results to", completion: .filename) + verbose = subparser.add(option: "--verbose", shortName: "-v", kind: Bool.self, usage: "Enable verbose logging") + + divideByModel = subparser.add(option: "--model", shortName: nil, kind: Bool.self, usage: "Divide attachments by model") + divideByOS = subparser.add(option: "--os", shortName: nil, kind: Bool.self, usage: "Divide attachments by OS") + divideByTestPlanRun = subparser.add(option: "--test-run", shortName: nil, kind: Bool.self, usage: "Divide attachments by test plan configuration") + + utiWhitelist = subparser.add(option: "--uti", shortName: nil, kind: [String].self, strategy: .upToNextOption, + usage: "Takes list of uniform type identifiers (UTI) and export only attachments that conform to at least one") + } + + func run(with arguments: ArgumentParser.Result) throws { + guard let xcresultPathArgument = arguments.get(path) else { + print("Missing xcresult path") + return + } + let xcresultPath = xcresultPathArgument.path + + var outputPath: AbsolutePath + if let outputPathArgument = arguments.get(self.outputPath) { + outputPath = outputPathArgument.path + } else if let workingDirectory = localFileSystem.currentWorkingDirectory { + outputPath = workingDirectory + } else { + print("Missing output path") + return + } + + let verbose = arguments.get(self.verbose) ?? false + + let xcpParser = XCPParser() + xcpParser.console.verbose = verbose + + var options = AttachmentExportOptions(addTestScreenshotsDirectory: false, + divideByTargetModel: arguments.get(self.divideByModel) ?? false, + divideByTargetOS: arguments.get(self.divideByOS) ?? false, + divideByTestRun: arguments.get(self.divideByTestPlanRun) ?? false) + if let allowedUTIsToExport = arguments.get(self.utiWhitelist) { + options.attachmentFilter = { + let attachmentUTI = $0.uniformTypeIdentifier as CFString + for allowedUTI in allowedUTIsToExport { + if UTTypeConformsTo(attachmentUTI, allowedUTI as CFString) { + return true + } + } + return false + } + } + + try xcpParser.extractAttachments(xcresultPath: xcresultPath.pathString, + destination: outputPath.pathString, + options: options) + } +} diff --git a/Sources/xcparse/CommandRegistry.swift b/Sources/xcparse/CommandRegistry.swift index d171ae2..fe0f375 100644 --- a/Sources/xcparse/CommandRegistry.swift +++ b/Sources/xcparse/CommandRegistry.swift @@ -62,8 +62,12 @@ struct CommandRegistry { let xcpParser = XCPParser() let options = AttachmentExportOptions(addTestScreenshotsDirectory: true, divideByTargetModel: false, - divideByTargetOS: false) - try xcpParser.extractScreenshots(xcresultPath: legacyScreenshotPaths[0].path.pathString, + divideByTargetOS: false, + divideByTestRun: false, + attachmentFilter: { + return UTTypeConformsTo($0.uniformTypeIdentifier as CFString, "public.image" as CFString) + }) + try xcpParser.extractAttachments(xcresultPath: legacyScreenshotPaths[0].path.pathString, destination: legacyScreenshotPaths[1].path.pathString, options: options) diff --git a/Sources/xcparse/ScreenshotsCommand.swift b/Sources/xcparse/ScreenshotsCommand.swift index be50336..ce77de5 100644 --- a/Sources/xcparse/ScreenshotsCommand.swift +++ b/Sources/xcparse/ScreenshotsCommand.swift @@ -63,8 +63,11 @@ struct ScreenshotsCommand: Command { let options = AttachmentExportOptions(addTestScreenshotsDirectory: arguments.get(self.addTestScreenshotDirectory) ?? false, divideByTargetModel: arguments.get(self.divideByModel) ?? false, divideByTargetOS: arguments.get(self.divideByOS) ?? false, - divideByTestRun: arguments.get(self.divideByTestPlanRun) ?? false) - try xcpParser.extractScreenshots(xcresultPath: xcresultPath.pathString, + divideByTestRun: arguments.get(self.divideByTestPlanRun) ?? false, + attachmentFilter: { + return UTTypeConformsTo($0.uniformTypeIdentifier as CFString, "public.image" as CFString) + }) + try xcpParser.extractAttachments(xcresultPath: xcresultPath.pathString, destination: outputPath.pathString, options: options) } diff --git a/Sources/xcparse/XCPParser.swift b/Sources/xcparse/XCPParser.swift index bbe8a7d..3eae839 100644 --- a/Sources/xcparse/XCPParser.swift +++ b/Sources/xcparse/XCPParser.swift @@ -11,7 +11,7 @@ import Foundation import SPMUtility import XCParseCore -let xcparseCurrentVersion = Version(0, 5, 1) +let xcparseCurrentVersion = Version(0, 6, 0) enum InteractiveModeOptionType: String { case screenshot = "s" @@ -73,6 +73,10 @@ struct AttachmentExportOptions { var divideByTargetOS: Bool = false var divideByTestRun: Bool = false + var attachmentFilter: (ActionTestAttachment) -> Bool = { _ in + return true + } + func baseScreenshotDirectoryURL(path: String) -> Foundation.URL { let destinationURL = URL.init(fileURLWithPath: path) if self.addTestScreenshotsDirectory { @@ -121,8 +125,8 @@ class XCPParser { // MARK: - // MARK: Parsing Actions - - func extractScreenshots(xcresultPath: String, destination: String, options: AttachmentExportOptions = AttachmentExportOptions()) throws { + + func extractAttachments(xcresultPath: String, destination: String, options: AttachmentExportOptions = AttachmentExportOptions()) throws { var xcresult = XCResult(path: xcresultPath, console: self.console) guard let invocationRecord = xcresult.invocationRecord else { return @@ -164,7 +168,7 @@ class XCPParser { } let testableSummaries = testPlanRun.testableSummaries - let testableSummariesAttachments = testableSummaries.flatMap { $0.attachments(withXCResult: xcresult) } + let testableSummariesAttachments = testableSummaries.flatMap { $0.attachments(withXCResult: xcresult) }.filter(options.attachmentFilter) // Now that we know what we want to export, save it to the dictionary so we can have all the exports // done at once with one progress bar per URL @@ -184,16 +188,16 @@ class XCPParser { let exportRelativePath = exportURL.path.replacingOccurrences(of: screenshotBaseDirectoryURL.path, with: "").trimmingCharacters(in: CharacterSet(charactersIn: "/")) let displayName = exportRelativePath.replacingOccurrences(of: "/", with: " - ") - self.exportScreenshots(withXCResult: xcresult, toDirectory: exportURL, attachments: attachmentsToExport, displayName: displayName) + self.exportAttachments(withXCResult: xcresult, toDirectory: exportURL, attachments: attachmentsToExport, displayName: displayName) } } - func exportScreenshots(withXCResult xcresult: XCResult, toDirectory screenshotDirectoryURL: Foundation.URL, attachments: [ActionTestAttachment], displayName: String = "") { + func exportAttachments(withXCResult xcresult: XCResult, toDirectory screenshotDirectoryURL: Foundation.URL, attachments: [ActionTestAttachment], displayName: String = "") { if attachments.count <= 0 { return } - let header = (displayName != "") ? "Exporting \"\(displayName)\" Screenshots" : "Exporting Screenshots" + let header = (displayName != "") ? "Exporting \"\(displayName)\" Attachments" : "Exporting Attachments" let progressBar = PercentProgressAnimation(stream: stdoutStream, header: header) progressBar.update(step: 0, total: attachments.count, text: "") @@ -310,6 +314,7 @@ class XCPParser { registry.register(command: ScreenshotsCommand.self) registry.register(command: CodeCoverageCommand.self) registry.register(command: LogsCommand.self) + registry.register(command: AttachmentsCommand.self) registry.register(command: VersionCommand.self) registry.run() @@ -338,7 +343,7 @@ class XCPParser { let path = console.getInput() console.writeMessage("Type the path to the destination folder for your screenshots:") let destinationPath = console.getInput() - try extractScreenshots(xcresultPath: path, destination: destinationPath) + try extractAttachments(xcresultPath: path, destination: destinationPath) case .log: console.writeMessage("Type the path to your *.xcresult file:") let path = console.getInput() diff --git a/xcparse.xcodeproj/project.pbxproj b/xcparse.xcodeproj/project.pbxproj index d49bd24..e797c53 100644 --- a/xcparse.xcodeproj/project.pbxproj +++ b/xcparse.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ /* Begin PBXBuildFile section */ 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 */; }; OBJ_180 /* ByteString.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_74 /* ByteString.swift */; }; OBJ_181 /* CStringArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_75 /* CStringArray.swift */; }; @@ -316,10 +317,18 @@ remoteGlobalIDString = "SwiftPM::clibc"; remoteInfo = clibc; }; + 62CC365723569927003C7B68 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = "xcparse::xcparseTests"; + remoteInfo = xcparseTests; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 62CC363D23553EA0003C7B68 /* XCResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCResult.swift; sourceTree = ""; }; + 62CC36582357C110003C7B68 /* AttachmentsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsCommand.swift; sourceTree = ""; }; OBJ_10 /* ActionDeviceRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionDeviceRecord.swift; sourceTree = ""; }; OBJ_100 /* ProcessEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessEnv.swift; sourceTree = ""; }; OBJ_101 /* ProcessSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessSet.swift; sourceTree = ""; }; @@ -742,7 +751,7 @@ name = Products; sourceTree = BUILT_PRODUCTS_DIR; }; - OBJ_5 /* */ = { + OBJ_5 = { isa = PBXGroup; children = ( OBJ_6 /* Package.swift */, @@ -755,7 +764,6 @@ OBJ_172 /* Makefile */, OBJ_173 /* README.md */, ); - name = ""; sourceTree = ""; }; OBJ_56 /* xcparse */ = { @@ -766,6 +774,7 @@ OBJ_59 /* CommandRegistry.swift */, OBJ_60 /* GitHubLatestReleaseResponse.swift */, OBJ_61 /* LogsCommand.swift */, + 62CC36582357C110003C7B68 /* AttachmentsCommand.swift */, OBJ_62 /* ScreenshotsCommand.swift */, OBJ_63 /* VersionCommand.swift */, OBJ_64 /* XCPParser.swift */, @@ -1123,7 +1132,7 @@ knownRegions = ( en, ); - mainGroup = OBJ_5 /* */; + mainGroup = OBJ_5; productRefGroup = OBJ_162 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1306,6 +1315,7 @@ OBJ_347 /* Command.swift in Sources */, OBJ_348 /* CommandRegistry.swift in Sources */, OBJ_349 /* GitHubLatestReleaseResponse.swift in Sources */, + 62CC36592357C110003C7B68 /* AttachmentsCommand.swift in Sources */, OBJ_350 /* LogsCommand.swift in Sources */, OBJ_351 /* ScreenshotsCommand.swift in Sources */, OBJ_352 /* VersionCommand.swift in Sources */, @@ -1412,7 +1422,7 @@ OBJ_376 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = "xcparse::xcparseTests" /* xcparseTests */; - targetProxy = "xcparse::xcparseTests" /* xcparseTests */; + targetProxy = 62CC365723569927003C7B68 /* PBXContainerItemProxy */; }; OBJ_390 /* PBXTargetDependency */ = { isa = PBXTargetDependency;