Skip to content

Commit

Permalink
Merge pull request #7 from hoangatuan/Support-custom-testing-frameworks
Browse files Browse the repository at this point in the history
Support custom testing frameworks
  • Loading branch information
hoangatuan authored Oct 2, 2023
2 parents 4ca9125 + 907e06f commit 79fc6ef
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 34 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/check-leaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
- name: Check out repository code
uses: actions/checkout@v4

- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 14.2.0
# - uses: maxim-lobanov/setup-xcode@v1.6.0
# with:
# xcode-version: 14.2.0

- name: ruby versions
run: |
Expand All @@ -31,11 +31,11 @@ jobs:
# ruby-version: 2.7
# bundler-cache: true

- run: xcodebuild -project MemoryLeaksCheck.xcodeproj -scheme MemoryLeaksCheck -destination 'platform=iOS Simulator,name=iPhone 14 Pro' CONFIGURATION_BUILD_DIR=$PWD/build
# - run: xcodebuild -project MemoryLeaksCheck.xcodeproj -scheme MemoryLeaksCheck -destination 'platform=iOS Simulator,name=iPhone 14 Pro' CONFIGURATION_BUILD_DIR=$PWD/build

- name: Check for leaks
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
run: ./LeaksDetector/leaksdetector MemoryLeaksCheck ./maestro/leaksCheckFlow.yaml
run: ./LeaksDetector/leaksdetector --process-name MemoryLeaksCheck --executor-type maestro --maestro-flow-path ./maestro/leaksCheckFlow.yaml -d ./Dangerfile.leaksReport

- run: echo "🍏 This job's status is ${{ job.status }}."
5 changes: 3 additions & 2 deletions LeaksDetector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Just need to run the script:

## How to run local

1. Open `LeaksDetector.swift`, change `processName` and `uiFlowFilePath` to a hardcoded value
1. Open `LeaksDetector.swift`, change `processName`, `executorType`, `maestroFlowPath`, `dangerPath` to a hardcoded value
2. Run:

```bash
Expand All @@ -37,7 +37,7 @@ Just need to run the script:
## Todo

1. Investigate how to integrate with XCUITest
2. Create protocol `UIExecutor` to support multiple kind of ui testing tools: Maestro, Appium, XCUITest ...
2. Integrate with DocC

## Why Maestro?

Expand All @@ -49,5 +49,6 @@ Just need to run the script:
- https://www.fivestars.blog/articles/a-look-into-argument-parser/
- https://www.swiftbysundell.com/articles/building-a-command-line-tool-using-the-swift-package-manager/
- https://www.avanderlee.com/swift/command-line-tool-package-manager/
- Enum arguments: https://swiftinit.org/docs/swift-argument-parser/argumentparser/customizingcompletions
- Split to multiple Danger instance: https://www.jessesquires.com/blog/2020/12/15/running-multiple-dangers/
- Passing params to Danger: https://github.com/danger/swift/issues/213
50 changes: 50 additions & 0 deletions LeaksDetector/Sources/LeaksDetector/Executor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// File.swift
//
//
// Created by Hoang Anh Tuan on 30/09/2023.
//

import Foundation
import ShellOut
import ArgumentParser

protocol Executor {
func simulateUI() throws
func generateMemgraph(for processName: String) throws
func getMemgraphPath() -> String
}

class DefaultExecutor: Executor {
let memgraphPath: String = "~/Desktop/Leaks.memgraph"

fileprivate init() { }

func simulateUI() throws {
fatalError("Need to override this func")
}

func generateMemgraph(for processName: String) throws {
try shellOut(to: "leaks \(processName) --outputGraph=\(memgraphPath)")
}

func getMemgraphPath() -> String {
return memgraphPath
}
}

final class MaestroExecutor: DefaultExecutor {

/// Maestro needs a flow.yaml to start simulating UI
private let flowPath: String

init(flowPath: String) {
self.flowPath = flowPath
super.init()
}

override func simulateUI() throws {
try shellOut(to: "maestro test \(flowPath)")
}
}

39 changes: 39 additions & 0 deletions LeaksDetector/Sources/LeaksDetector/ExecutorFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// File.swift
//
//
// Created by Hoang Anh Tuan on 30/09/2023.
//

import Foundation
import ArgumentParser

enum ExecutorType: String, CaseIterable, Codable, ExpressibleByArgument {
case maestro
// case xcuitest

static var supportedTypesDescription: String {
"Current support types are: maestro"
}
}

enum ParameterKeys {
static let maestroFilePath: String = "maestroFlowPath"
}

typealias ExecutorParameters = [String: Any]
enum ExecutorFactory {
static func createExecutor(
for type: ExecutorType,
parameters: ExecutorParameters = [:]
) -> Executor? {
switch type {
case .maestro:
guard let flowPath = parameters[ParameterKeys.maestroFilePath] as? String else {
log(message: "❌ Used of type `maestro` will need to specify --maestro-flow-path", color: .red)
return nil
}
return MaestroExecutor(flowPath: flowPath)
}
}
}
67 changes: 43 additions & 24 deletions LeaksDetector/Sources/LeaksDetector/LeaksDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import Foundation
import ArgumentParser
import ShellOut
import TSCBasic
import TSCUtility
import Darwin

@main
struct LeaksDetector: ParsableCommand {
Expand All @@ -14,40 +13,59 @@ struct LeaksDetector: ParsableCommand {

#if DEBUG
private var processName = "MemoryLeaksCheck"
private var uiFlowFilePath = "/Users/hoanganhtuan/Desktop/MemoryLeaksCheck/maestro/leaksCheckFlow.yaml"
private var executorType: ExecutorType = .maestro
private var maestroFlowPath: String? = "/Users/hoanganhtuan/Desktop/MemoryLeaksCheck/maestro/leaksCheckFlow.yaml"
private var dangerPath: String = "Dangerfile.leaksReport"
#else
@Argument(help: "The name of the running process")
private var processName: String

@Argument(help: "The path to the maestro ui testing yaml file")
private var uiFlowFilePath: String


@Option(name: .long, help: "The name of the running process")
private var processName: String

@Option(name: .shortAndLong, help: "The testing tools you want to use. \(ExecutorType.supportedTypesDescription)")
private var executorType: ExecutorType

@Option(name: .long, help: "The path to the maestro ui testing yaml file")
private var maestroFlowPath: String?

@Option(name: .shortAndLong, help: "The path to the Dangerfile")
private var dangerPath: String
#endif

// @Flag(name: .long, help: "Show extra logging for debugging purposes")
// private var verbose: Bool = false

private var memgraphPath = "~/Desktop/Leaks.memgraph"
private var regex: String = ".*(\\d+) leaks for (\\d+) total leaked bytes.*"

func run() throws {
mutating func run() throws {
guard let executor = ExecutorFactory.createExecutor(for: executorType, parameters: prepareParams()) else {
Darwin.exit(EXIT_FAILURE)
}

log(message: "Start looking for process with name: \(processName)... 🔎")

if !runningMaestro() { return }
if !generateMemgraph(for: processName) { return }
if !simulateUIFlow(by: executor) {
Darwin.exit(EXIT_FAILURE)
}

if !generateMemgraph(by: executor) {
Darwin.exit(EXIT_FAILURE)
}

do {
try checkLeaks()
try checkLeaks(by: executor)
} catch {
log(message: "❌ Error occurs while checking for leaks", color: .red)
Darwin.exit(EXIT_FAILURE)
}
}

private func runningMaestro() -> Bool {
private func prepareParams() -> ExecutorParameters {
var params: [String: String] = [:]
params[ParameterKeys.maestroFilePath] = maestroFlowPath
return params
}

private func simulateUIFlow(by executor: Executor) -> Bool {
log(message: "Start running ui flow... 🎥")
do {
try shellOut(to: "maestro test \(uiFlowFilePath)")
try executor.simulateUI()
return true
} catch {
let error = error as! ShellOutError
Expand All @@ -56,9 +74,9 @@ struct LeaksDetector: ParsableCommand {
}
}

private func generateMemgraph(for processName: String) -> Bool {
private func generateMemgraph(by executor: Executor) -> Bool {
do {
try shellOut(to: "leaks \(processName) --outputGraph=\(memgraphPath)")
try executor.generateMemgraph(for: processName)
log(message: "Generate memgraph successfully for process 🚀", color: .green)
return true
} catch {
Expand All @@ -67,9 +85,10 @@ struct LeaksDetector: ParsableCommand {
}
}

private func checkLeaks() throws {
private func checkLeaks(by executor: Executor) throws {
do {
log(message: "Start checking for leaks... ⚙️")
let memgraphPath = executor.getMemgraphPath()
try shellOut(to: "leaks", arguments: ["\(memgraphPath) -q"])
} catch {
let error = error as! ShellOutError
Expand All @@ -95,10 +114,10 @@ struct LeaksDetector: ParsableCommand {
// Cache memgraphfile if need

log(message: "Founded leaks. Generating reports... ⚙️")
try shellOut(to: "bundle exec danger --dangerfile=Dangerfile.leaksReport --danger_id=LeaksReport")
try shellOut(to: "bundle exec danger --dangerfile=\(dangerPath) --danger_id=LeaksReport")

log(message: "Cleaning... 🧹")
_ = try? shellOut(to: "rm \(memgraphPath)")
_ = try? shellOut(to: "rm \(executor.getMemgraphPath())")
_ = try? shellOut(to: "rm \(fileName)")

log(message: "Done ✅", color: .green)
Expand Down
Binary file modified LeaksDetector/leaksdetector
Binary file not shown.
Binary file not shown.
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ Learn more about `Maestro` [here](https://maestro.mobile.dev/)
3. In your ci workflow, just call:

```bash
leaksdetector $YOUR_APP_NAME $PATH_TO_MAESTRO_FILE
leaksdetector -processName $YOUR_APP_NAME -e $SUPPORTED_TESTING_FRAMEWORKS -d $PATH_TO_DANGER_FILE
```

## Current support testing frameworks

- [Maestro](https://maestro.mobile.dev/)
- [XCUITest](https://developer.apple.com/documentation/xctest) (In progress)

## How it works

1. Use Maestro to simulate the UI flow in your app.
Expand All @@ -30,11 +35,40 @@ Find more about `leaks` tool and `memgraph` [here](https://developer.apple.com/v

## Why I used Maestro?

I need to testing tool which doesn't kill the program after the testing finished execution. And Maestro support that. Also Maestro is very to integrated & used.
1. I need a testing tool which doesn't kill the program after the testing finished execution. And Maestro support that. Also Maestro is very easy to integrate & use.
2. I've tried to used XCUItest, which is really promissing. Based on this [WWDC video](https://developer.apple.com/videos/play/wwdc2021/10180/) from Apple, XCUITest even *allows us to capture the stacktrace where leaks occur & generate a memgraph*. However, I've tried to follow the video but Xcode didn't generate any memgraph.
=> I'm working on this.


## How to support your testing frameworks

If you're using another UI testing framework which also support preserve the execution of the program after finish testing, you can create another PR to update the `leaksdetector`.
It's easy to do that, just need to follow these steps:

TBC (Not for now, I will need to refactor to support that first)
1. Open `Executor.swift`, create a new instance of your testing frameworks. Your new instance needs to conform to `Executor` protocol.

```swift

struct XCUITestExecutor: Executor {

func simulateUI() throws {
// Custom logic to start simulating UI
}

func generateMemgraph(for processName: String) throws {
// Custom logic to start generating memgraph for a `processName`
}

func getMemgraphPath() -> String {
// return the path to the generated memgraph
}
}

```

2. Open `ExecutorFactory.swift`, define your new UI testing frameworks to the `ExecutorType`, and add logic to generate it in the `createExecutor` func.

3. Add new `@Option` to the executable program if need

## Result

Expand Down
13 changes: 13 additions & 0 deletions temporary.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
leaks Report Version: 4.0
Process 3651: 42333 nodes malloced for 6720 KB
Process 3651: 4 leaks for 160 total leaked bytes.

4 (160 bytes) << TOTAL >>

2 (64 bytes) ROOT CYCLE: <Apartment 0x600003afd580> [32]
1 (32 bytes) person --> ROOT CYCLE: <Person 0x600003afd5e0> [32]
apartment --> CYCLE BACK TO <Apartment 0x600003afd580> [32]

1 (48 bytes) ROOT LEAK: <HomeViewModel 0x6000034bcd20> [48]
1 (48 bytes) ROOT LEAK: <HomeViewModel 0x6000034bd320> [48]

0 comments on commit 79fc6ef

Please sign in to comment.