Skip to content

Commit

Permalink
Add attachments sub-command & allow attachment UTI filtering (#25)
Browse files Browse the repository at this point in the history
Change Description: These changes add a new "attachments" sub-command. This command will by default export all attachments in the xcresult regardless of uniform type identifier (what screenshots used to be doing). Users can provide a list of UTIs to filter with (ex. "--uti public.plain-text") to only export attachments which conform to the given UTI. This would be useful for folks who want to get all the debug descriptions, for example.

The screenshots command has bee modified so that it'll only export image attachments & not other files such as text debug descriptions from test failures. It does this by taking advantage of the UTI filtering & using "public.image" UTI.

Test Plan/Testing Performed: Tested that with these changes, I can now export attachments that conform to plain text or image UTIs and avoid exporting everything.
  • Loading branch information
abotkin-cpi authored Oct 17, 2019
1 parent 3e2d6c5 commit d09a5e9
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 24 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

```
Expand All @@ -53,6 +65,8 @@ xcparse --help
xcparse screenshots --help
```

Learn about all the options we didn't mention with ```--help```!

## Modes

### Static Mode
Expand Down
8 changes: 1 addition & 7 deletions Sources/XCParseCore/ActionTestAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
86 changes: 86 additions & 0 deletions Sources/xcparse/AttachmentsCommand.swift
Original file line number Diff line number Diff line change
@@ -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<PathArgument>
var outputPath: PositionalArgument<PathArgument>
var verbose: OptionArgument<Bool>

var divideByModel: OptionArgument<Bool>
var divideByOS: OptionArgument<Bool>
var divideByTestPlanRun: OptionArgument<Bool>

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)
}
}
8 changes: 6 additions & 2 deletions Sources/xcparse/CommandRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 5 additions & 2 deletions Sources/xcparse/ScreenshotsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
21 changes: 13 additions & 8 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(0, 5, 1)
let xcparseCurrentVersion = Version(0, 6, 0)

enum InteractiveModeOptionType: String {
case screenshot = "s"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: "")

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down
18 changes: 14 additions & 4 deletions xcparse.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<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>"; };
OBJ_100 /* ProcessEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessEnv.swift; sourceTree = "<group>"; };
OBJ_101 /* ProcessSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessSet.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -742,7 +751,7 @@
name = Products;
sourceTree = BUILT_PRODUCTS_DIR;
};
OBJ_5 /* */ = {
OBJ_5 = {
isa = PBXGroup;
children = (
OBJ_6 /* Package.swift */,
Expand All @@ -755,7 +764,6 @@
OBJ_172 /* Makefile */,
OBJ_173 /* README.md */,
);
name = "";
sourceTree = "<group>";
};
OBJ_56 /* xcparse */ = {
Expand All @@ -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 */,
Expand Down Expand Up @@ -1123,7 +1132,7 @@
knownRegions = (
en,
);
mainGroup = OBJ_5 /* */;
mainGroup = OBJ_5;
productRefGroup = OBJ_162 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit d09a5e9

Please sign in to comment.