Skip to content

Commit

Permalink
Allow screenshot export to be divisible by model, OS, & test run conf…
Browse files Browse the repository at this point in the history
…iguration (#23)

Change Description: Introducing changes to implement #21. These changes introduce new options on "xcparse screenshots" to allow for division of screenshots by model, OS version, & test plan run configuration. Each of these is a new optional argument that can be added so that users of xcparse have choice about what kind of division they want. So if a user wanted to just export by model, they could run "xcparse screenshots --model ". If they want division by model, OS, & test plan run config, they'd run "xcparse screenshots --model --os --test-run "

Test Plan/Testing Performed: Ran tests on another project with multiple devices & OS versions & test plan configurations. Saw that the screenshots followed the division given by the options provided. Also ensured that the progress bars for each would consolidate appropriately where users would expect.
  • Loading branch information
abotkin-cpi authored Oct 16, 2019
1 parent 0448246 commit 0b0c247
Show file tree
Hide file tree
Showing 14 changed files with 2,412 additions and 3,190 deletions.
Binary file added Docs/Images/screenshots_options_recommended.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ Below are a few examples of common commands. For further assistance, use the --h
### Screenshots

```
xcparse screenshots /path/to/Test.xcresult /path/to/exportScreenshots
xcparse screenshots --os --model --test-run /path/to/Test.xcresult /path/to/exportScreenshots
```

This will cause screenshots to be exported like so:

![Screenshots exported into folders](Docs/Images/screenshots_options_recommended.png?raw=true)

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.

### Code Coverage

```
Expand Down
1 change: 1 addition & 0 deletions Sources/XCParseCore/ActionTestPlanRunSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation

open class ActionTestPlanRunSummary : ActionAbstractTestSummary {
// Note: name is inherited from ActionAbstractTestSummary & will contain the test run configuration name
public let testableSummaries: [ActionTestableSummary]

enum ActionTestPlanRunSummaryCodingKeys: String, CodingKey {
Expand Down
14 changes: 14 additions & 0 deletions Sources/XCParseCore/ActionTestSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,18 @@ open class ActionTestSummary : ActionTestSummaryIdentifiableObject {

try super.init(from: decoder)
}

public func attachments() -> [ActionTestAttachment] {
var activitySummaries = self.activitySummaries

var summariesToCheck = activitySummaries
repeat {
summariesToCheck = summariesToCheck.flatMap { $0.subactivities }

// Add the subactivities we found
activitySummaries.append(contentsOf: summariesToCheck)
} while summariesToCheck.count > 0

return activitySummaries.flatMap { $0.attachments }
}
}
68 changes: 68 additions & 0 deletions Sources/XCParseCore/ActionTestableSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,72 @@ open class ActionTestableSummary : ActionAbstractTestSummary {

try super.init(from: decoder)
}

public func attachments(withXCResult xcresult: XCResult) -> [ActionTestAttachment] {
var attachments: [ActionTestAttachment] = []

var tests: [ActionTestSummaryIdentifiableObject] = self.tests

var testSummaries: [ActionTestSummary] = []
var testMetadata: [ActionTestMetadata] = []

// Iterate through the testSummaryGroups until we get out all the test summaries & metadata
repeat {
let summaryGroups = tests.compactMap { (identifiableObj) -> ActionTestSummaryGroup? in
if let testSummaryGroup = identifiableObj as? ActionTestSummaryGroup {
return testSummaryGroup
} else {
return nil
}
}

let summaries = tests.compactMap { (identifiableObj) -> ActionTestSummary? in
if let testSummary = identifiableObj as? ActionTestSummary {
return testSummary
} else {
return nil
}
}
testSummaries.append(contentsOf: summaries)

let metadata = tests.compactMap { (identifiableObj) -> ActionTestMetadata? in
if let metadata = identifiableObj as? ActionTestMetadata {
return metadata
} else {
return nil
}
}
testMetadata.append(contentsOf: metadata)

tests = summaryGroups.flatMap { $0.subtests }
} while tests.count > 0

// Need to extract out the testSummary until get all ActionTestActivitySummary
var activitySummaries = testSummaries.flatMap { $0.activitySummaries }

// Get all subactivities
var summariesToCheck = activitySummaries
repeat {
summariesToCheck = summariesToCheck.flatMap { $0.subactivities }

// Add the subactivities we found
activitySummaries.append(contentsOf: summariesToCheck)
} while summariesToCheck.count > 0

for activitySummary in activitySummaries {
let summaryAttachments = activitySummary.attachments
attachments.append(contentsOf: summaryAttachments)
}

let testSummaryReferences = testMetadata.compactMap { $0.summaryRef }
for summaryReference in testSummaryReferences {
guard let summary: ActionTestSummary = summaryReference.modelFromReference(withXCResult: xcresult) else {
xcresult.console.writeMessage("Error: Unhandled test summary type \(String(describing: summaryReference.targetType?.getType()))", to: .error)
continue
}
attachments.append(contentsOf: summary.attachments())
}

return attachments
}
}
18 changes: 18 additions & 0 deletions Sources/XCParseCore/ActionsInvocationRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,22 @@ public class ActionsInvocationRecord : Codable {

archive = try container.decodeXCResultObjectIfPresent(forKey: .archive)
}

class public func recordFromXCResult(_ xcresult: XCResult) -> ActionsInvocationRecord? {
guard let xcresultGetResult = XCResultToolCommand.Get(withXCResult: xcresult, id: "", outputPath: "", format: .json).run() else {
return nil
}

do {
let xcresultJSON = try xcresultGetResult.utf8Output()
if xcresultGetResult.exitStatus != .terminated(code: 0) || xcresultJSON == "" {
return nil
}

let xcresultJSONData = Data(xcresultJSON.utf8)
return try JSONDecoder().decode(ActionsInvocationRecord.self, from: xcresultJSONData)
} catch {
return nil
}
}
}
22 changes: 22 additions & 0 deletions Sources/XCParseCore/Reference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,26 @@ open class Reference : Codable {
id = try container.decodeXCResultType(forKey: .id)
targetType = try container.decodeXCResultObjectIfPresent(forKey: .targetType)
}

public func modelFromReference<T: Codable>(withXCResult xcresult: XCResult) -> T? {
if self.targetType?.getType() != T.self {
return nil
}

guard let summaryGetResult = XCResultToolCommand.Get(withXCResult: xcresult, id: self.id, outputPath: "", format: .json).run() else {
return nil
}

do {
let jsonString = try summaryGetResult.utf8Output()
if summaryGetResult.exitStatus != .terminated(code: 0) || jsonString == "" {
return nil
}

let referenceData = Data(jsonString.utf8)
return try JSONDecoder().decode(T.self, from: referenceData)
} catch {
return nil
}
}
}
2 changes: 1 addition & 1 deletion Sources/XCParseCore/TypeDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ open class TypeDefinition : Codable {
supertype = try container.decodeXCResultObjectIfPresent(forKey: .supertype)
}

func getType() -> AnyObject.Type {
public func getType() -> AnyObject.Type {
if let type = XCResultTypeFamily(rawValue: self.name) {
return type.getType()
} else if let parentType = self.supertype {
Expand Down
19 changes: 19 additions & 0 deletions Sources/XCParseCore/XCResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// XCResult.swift
// XCParseCore
//
// Created by Alex Botkin on 10/14/19.
//

import Foundation

public struct XCResult {
public let path: String
public let console: Console
public lazy var invocationRecord: ActionsInvocationRecord? = ActionsInvocationRecord.recordFromXCResult(self)

public init(path xcresultPath: String, console: Console = Console()) {
self.path = xcresultPath
self.console = console
}
}
59 changes: 29 additions & 30 deletions Sources/XCParseCore/XCResultToolCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ let xcresultToolArguments = ["xcrun", "xcresulttool"]
open class XCResultToolCommand {
let process: Basic.Process

let console: Console
let xcresult: XCResult
var console: Console {
get {
return self.xcresult.console
}
}

public init(withConsole console: Console = Console(), process: Basic.Process = Basic.Process(arguments: ["xcrun", "xcresulttool", "-h"])) {
self.console = console
public init(withXCResult xcresult: XCResult, process: Basic.Process = Basic.Process(arguments: ["xcrun", "xcresulttool", "-h"])) {
self.xcresult = xcresult
self.process = process
}

Expand Down Expand Up @@ -54,32 +59,28 @@ open class XCResultToolCommand {
case file = "file"
case directory = "directory"
}

var path: String = ""

var id: String = ""
var outputPath: String = ""
var type: ExportType = ExportType.file

public init(path: String, id: String, outputPath: String, type: ExportType, console: Console = Console()) {
self.path = path
public init(withXCResult xcresult: XCResult, id: String, outputPath: String, type: ExportType) {
self.id = id
self.outputPath = outputPath
self.type = type

var processArgs = xcresultToolArguments
processArgs.append(contentsOf: ["export",
"--type", self.type.rawValue,
"--path", self.path,
"--path", xcresult.path,
"--id", self.id,
"--output-path", self.outputPath])

let process = Basic.Process(arguments: processArgs)
super.init(withConsole: console, process: process)
super.init(withXCResult: xcresult, process: process)
}

public init(path: String, attachment: ActionTestAttachment, outputPath: String, console: Console = Console()) {
self.path = path

public init(withXCResult xcresult: XCResult, attachment: ActionTestAttachment, outputPath: String) {
if let identifier = attachment.payloadRef?.id {
self.id = identifier;

Expand All @@ -92,30 +93,33 @@ open class XCResultToolCommand {
var processArgs = xcresultToolArguments
processArgs.append(contentsOf: ["export",
"--type", self.type.rawValue,
"--path", self.path,
"--path", xcresult.path,
"--id", self.id,
"--output-path", self.outputPath])

let process = Basic.Process(arguments: processArgs)
super.init(withConsole: console, process: process)
super.init(withXCResult: xcresult, process: process)
}
}

open class Get: XCResultToolCommand {
var path: String = ""
var id: String = ""
var outputPath: String = ""
var format = FormatType.raw

public init(path: String, id: String, outputPath: String, format: FormatType, console: Console = Console()) {
self.path = path
convenience public init(path: String, id: String, outputPath: String, format: FormatType, console: Console = Console()) {
let xcresult = XCResult(path: path, console: console)
self.init(withXCResult: xcresult, id: id, outputPath: outputPath, format: format)
}

public init(withXCResult xcresult: XCResult, id: String, outputPath: String, format: FormatType) {
self.id = id
self.outputPath = outputPath
self.format = format

var processArgs = xcresultToolArguments
processArgs.append(contentsOf: ["get",
"--path", self.path,
"--path", xcresult.path,
"--format", self.format.rawValue])
if self.id != "" {
processArgs.append(contentsOf: ["--id", self.id])
Expand All @@ -125,23 +129,21 @@ open class XCResultToolCommand {
}

let process = Basic.Process(arguments: processArgs)
super.init(withConsole: console, process: process)
super.init(withXCResult: xcresult, process: process)
}
}

open class Graph: XCResultToolCommand {
var id: String = ""
var path: String = ""
var version: Int?

public init(id: String, path: String, version: Int?, console: Console = Console()) {
public init(withXCResult xcresult: XCResult, id: String, version: Int?) {
self.id = id
self.path = path
self.version = version

var processArgs = xcresultToolArguments
processArgs.append(contentsOf: ["graph",
"--path", self.path])
"--path", xcresult.path])
if self.id != "" {
processArgs.append(contentsOf: ["--id", self.id])
}
Expand All @@ -150,22 +152,19 @@ open class XCResultToolCommand {
}

let process = Basic.Process(arguments: processArgs)
super.init(withConsole: console, process: process)
super.init(withXCResult: xcresult, process: process)
}
}

open class MetadataGet: XCResultToolCommand {
var path: String = ""

public init(path: String, console: Console = Console()) {
self.path = path

public init(withXCResult xcresult: XCResult) {
var processArgs = xcresultToolArguments
processArgs.append(contentsOf: ["metadata", "get",
"--path", self.path])
"--path", xcresult.path])

let process = Basic.Process(arguments: processArgs)
super.init(withConsole: console, process: process)
super.init(withXCResult: xcresult, process: process)
}
}
}
5 changes: 4 additions & 1 deletion Sources/xcparse/CommandRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ struct CommandRegistry {

do {
let xcpParser = XCPParser()
let options = AttachmentExportOptions(addTestScreenshotsDirectory: true,
divideByTargetModel: false,
divideByTargetOS: false)
try xcpParser.extractScreenshots(xcresultPath: legacyScreenshotPaths[0].path.pathString,
destination: legacyScreenshotPaths[1].path.pathString,
options: AttachmentExportOptions(folderStructure: .legacy))
options: options)

return true
} catch {
Expand Down
18 changes: 17 additions & 1 deletion Sources/xcparse/ScreenshotsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,23 @@ struct ScreenshotsCommand: Command {
var outputPath: PositionalArgument<PathArgument>
var verbose: OptionArgument<Bool>

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

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")

addTestScreenshotDirectory = subparser.add(option: "--legacy", shortName: nil, kind: Bool.self, usage: "Create \"testScreenshots\" directory in outputDirectory & put screenshots in there")
divideByModel = subparser.add(option: "--model", shortName: nil, kind: Bool.self, usage: "Divide screenshots by model")
divideByOS = subparser.add(option: "--os", shortName: nil, kind: Bool.self, usage: "Divide screenshots by OS")
divideByTestPlanRun = subparser.add(option: "--test-run", shortName: nil, kind: Bool.self, usage: "Divide screenshots by test plan configuration")
}

func run(with arguments: ArgumentParser.Result) throws {
Expand All @@ -49,7 +59,13 @@ struct ScreenshotsCommand: Command {

let xcpParser = XCPParser()
xcpParser.console.verbose = verbose

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,
destination: outputPath.pathString)
destination: outputPath.pathString,
options: options)
}
}
Loading

1 comment on commit 0b0c247

@kenji21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

Please sign in to comment.