Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mockolo: Efficient Mock Generator for Swift

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →

Mockolo: Efficient Mock Generator for Swift

When writing code to fit a certain design pattern, creating data models, or writing mocks for unit tests, it’s sometimes hard to avoid writing templated code. Especially when the codebase gets larger, it becomes very cumbersome to write them manually. This is when a code generation tool can be very handy.

At Uber, we have millions of lines of code, and needed a tool that transforms code in a performant, scalable manner. This talk will go over how we built such tool, with a focus on mock generation. We’ll discuss the details of each step in the tool development, various optimizations applied to make it performant, as well as how the tool can be used in your app.

Avatar for ellsk1

ellsk1

May 27, 2019

More Decks by ellsk1

Other Decks in Technology

Transcript

  1. Useful for a large amount of templated / repetitive code

    Can be built with a minimal set of frameworks Keep it Lean and Performant Code Generation
  2. • Large codebase: ~2.3M LoC • Multiple apps / Multiple

    features: 350 modules • Complicated dependencies • Template-heavy design patterns @Uber • Built multiple codegen tools ◦ Dependency Injection Automator ◦ Model Generator ◦ Resource Generator ◦ Mock Generator ◦ … and more
  3. Mocking in Swift • Static Language: Safety / Security •

    Limited / Read-only Runtime Access • Makes Mocking Difficult, unlike Objective-C • Use OCMock? ◦ Need to subclass NSObject ◦ Add @objc to all override methods ◦ No generics ◦ No class method mocking • Swift Annotation?
  4. Other Mocking Frameworks Run on 2.3M LoC / 11K Protocols

    -> Output 0.5M LoC / 9K Mocks Goal: < 10s / Integrate into builds Name No Caching Caching Sourcery ~1.2h ~340s Cuckoo ~2h+ --- < Measured on MBP 6 core 2018 >
  5. Mock Output /// @mockable public protocol Foo: Bar { func

    visit(node: Node) -> Int } public class FooMock: Foo { } public init() { } var visitCallCount = 0 var visitHandler: ((Node)->(Int))? public func visit(node: Node) -> Int { visitCallCount += 1 if let visitHandler = visitHandler { return visitHandler(node) } return -1 }
  6. Parser SwiftSyntax SourceKit Slow performance: ~20x slower Fast: 1~2 ms

    per file Launches swiftc per file Sourcekit daemon keeps running after launched No extensive type info avialable Extensive type info: slow but available
  7. Sourcekitten /// @mockable public protocol Foo: Bar { func visit(node:

    Node) -> Int } Annotation lookup key.doclength / key.docoffset Protocol lookup key.kind = source.lang.swift.decl.protocol "key.length" : 89, "key.offset" : 0, "key.substructure" : [ { "key.accessibility" : "source.lang.swift.accessibility.public", "key.attributes" : [{ "key.attribute" : "source.decl.attribute.public", "key.length" : 6, "key.offset" : 25 }], "key.doclength" : 25, "key.docoffset" : 12, "key.inheritedtypes" : [{ "key.name" : "Bar" }], "key.runtime_name" : "_TtP4main3Foo_", "key.kind" : "source.lang.swift.decl.protocol", "key.length" : 56, "key.name" : "Foo", "key.offset" : 32, "key.substructure" : [ { "key.length" : 29, "key.namelength" : 17, "key.nameoffset" : 62, "key.offset" : 57, "key.accessibility" : "source.lang.swift.accessibility.public", "key.kind" : "source.lang.swift.decl.function.method.instance", "key.name" : "visit(node:)", "key.substructure" : [ { "key.kind" : "source.lang.swift.decl.var.parameter", "key.length" : 10, "key.name" : "node", "key.offset" : 68, "key.typename" : "Node" } ], "key.typename" : "Int" } ] } key.doclength: 25 key.docoffset: 12 key.kind: source.lang.swift. decl.protocol
  8. Output: Candidate Protocols / Metadata Protocol? Foo Bar, AST/Imports/Meta Bar

    Baz, AST/Imports/Meta Baz -----, AST/Imports/Meta Candidate Protocols Parse AST Annotated?
  9. Concurrency Scan Dirs Protocol? … … … Parse File AST

    Parse File Parse File AST AST Protocol Entities Annotated? Protocol? Annotated? Protocol? Annotated?
  10. let semaphore = DispatchSemaphore(value: maxThreads) for filePath in paths {

    _ = semaphore.wait(timeout: DispatchTime.distantFuture()) queue.async { . . . } semaphore.signal() } Throttle Concurrency = # cores
  11. struct Task { init(taskHandler: @escaping (TaskHandler, TaskResult) -> ()){ self.taskHandler

    = taskHandler } } Tasks struct TaskHandler { let queue: DispatchQueue let handler:(TaskResultType)->() } static func work(ast: Structure, path: String) -> Task { return Task { (resultHandler, _) in if ast.isProtocol, ast.isAnnotated { resultHandler.finish(ret: ast) } } } func perform(on queue: DispatchQueue, handler: @escaping (TaskResultType) -> ()) { queue.async { self.taskHandler(TaskHandler(queue, handler), ...) } } func finish(ret:[TaskResult]?) { handler(.success(ret)) } func fail(with error: Error) { handler(.failure(error)) }
  12. Grouping Tasks let x = listX.map { Task.workX(...) } let

    y = listY.map { Task.workY(...) } let group = Task.group(x + y) group.perform(on: queue) { // handle completion } static func group(_ tasks: [Task]) -> Task { let group = DispatchGroup() for task in tasks { group.enter() task.perform(on: taskHandler.queue) { ret in switch result { ... } group.leave() } } group.notify(queue: taskHandler.queue) { taskHandler.finish(with: taskResults) } }
  13. Chaining Tasks let g1 = Task.group(x + y) let g2

    = Task.group(z + w) let chain = Task.chain([g1, g2]) chain.perform(on: queue) { // handle completion } static func chain(_ tasks: [Task]) -> Task { func runNext(using handler: TaskHandler, results: [TaskResult]?) { guard index < tasks.count else { handler.finish(with: results) return } let task = tasks[index + 1] task.perform(on: handler.queue) { ret in switch ret { case .success(let result): runNext(using: handler, results: result) ... } return Task(taskHandler: runNext) }
  14. Resolve Inheritance /// @mockable protocol Foo: Bar { func visit(node:

    Node) -> Int } protocol Bar: Baz { } protocol Baz { }
  15. Foo Bar Baz Qux Quux Corge Grault Garply Waldo Plugh

    Thud Wibble Wobble Flob . . . . . . . . . . . . . . . . . . . . . . .
  16. Bar Baz Qux Quux Corge Grault Garply Waldo Plugh Thud

    Wibble Wobble Flob BarMocks.swift Foo
  17. • Useful for Inter-module protocol inheritance dependency • Dependency mocks

    as input • Pre-compute dependencies • Leverage build system
  18. protocol Foo { func visit(node: Node) -> Int // var

    visitNodeCallCount func visit(node: Node) -> String var visitNodeStringCallCount func visit(node123: Node) -> Int var visitNode123CallCount func visit(node: Node, hasChild: Bool) -> Int var visitNodeHasChildCallCount } Uniquify Models
  19. protocol Foo: Bar { func visit(node: Node) -> Int }

    var visitNodeCallCount protocol Bar { func visit(node: Node) -> Int } ➢ Overloading
  20. /// @mockable protocol Foo: Bar, Baz { var coffee: Int

    { get set } } protocol Bar { var coffee: Int { get set } // } protocol Baz { var donut: String { get set }// } ➢ Initializer Vars class FooMock: Foo { init(coffee: Int, donut: String) { self.coffee = coffee self.donut = donut } var coffee: Int { ... } var donut: String { ... }
  21. Templates String Interpolation for t in types { if t.isAnnotated

    { let template = """ class \(t.name)Mock: \(t.name) { \(t.initializerBlock) \(t.entities) } """ } } Stencil {% for t in types %} {% if t.isAnnotated %} {% call handleMethod t %} {% endif %} {% endfor %} {% macro handleMethod t %} ... {% endmacro %}
  22. func f() -> Int Default Return Values var fHandler: (()

    -> (Int))? func f() -> (Int) { if let fHandler = fHandler { fHandler() } return 0 // vs. fatalError() }
  23. func f() -> (Int, String?) // (0, nil) func f()

    -> (lhs: Int, rhs: String) // (lhs: 0, rhs: “”) func f() -> (Int, (String, (Double, (Int, Float)))) // (0, ("", (0.0, (0, 0)))) Tuple Parsing
  24. func f() -> Array<String> // Array<String>() func f() -> [String:

    (Int, Double)] // [String: (Int, Double)]() func f() -> (Int, Dictionary<Int, Double>) // (0, Dictionary<Int,Double>()) func f() -> (Int, (Double, [String:[Int]])) // (0, (0.0, [String: [Int]]())) func f() -> [(Int, Float, [(String, Int)])] // [(Int, Float, [(String, Int)])]() ((([ , ( , )], ( , [< , >]), ([ : ], ( , )), )))
  25. import ModuleX import ModuleY @available(iOS 10.0, *) public class FooMock:

    Foo { init(coffee: String) { self.coffee = coffee } // Entries from Foo var fHandler: ((UNUserNotificationCenter) -> ())? func f(arg: UNUserNotificationCenter) { } // Entries from Bar var coffee: String { ... } } /// @mockable public protocol Foo: Bar { @available(iOS 10.0, *) func f(arg: UNUserNotificationCenter) } public protocol Bar { var coffee: String { get set } } Final Output
  26. Profile APIs Slower Faster Diff string.split(separator: ",") string.components(separatedBy: ",") 2x

    string.count string.utf8.count 280x URL(fileURLWithPath: path) URL(fileURLWithPath: path, isDirectory: false) 6x arg as? VariableModel filter(arg.isVariableModel) 20x
  27. Results Run on 2.3M LoC / 11K Protocols -> 0.5M

    LoC / 9K Mocks Output < Measured on MBP 6 core 2018 > Name No Caching Caching Sourcery ~1.2h ~340s Cuckoo ~2h+ --- Mockolo (concurrent) ~5.3s --- Mockolo (serial) ~17s ---
  28. Summary • Leverage concurrency • Filter unneeded types or files

    / Lazily generate models • Pre-compute dependency tree where applicable • Profile standard lib APIs • Template: String interpolation • Use min / lightweight frameworks
  29. • Mockolo at github.com/uber • Install via Swift Package Manager

    • Run ./mockolo -srcs [source files] -out [output filepath] Twitter: @ellsk1 Stay Tuned
  30. Go build your own Keep it Lean and Performant Try

    out mockolo Feedback / contributions welcome!
  31. Proprietary and confidential © 2019 Uber Technologies, Inc. All rights

    reserved. No part of this document may be reproduced or utilized in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval systems, without permission in writing from Uber. This document is intended only for the use of the individual or entity to whom it is addressed and contains information that is privileged, confidential or otherwise exempt from disclosure under applicable law. All recipients of this document are notified that the information contained herein includes proprietary and confidential information of Uber, and recipient may not make use of, disseminate, or in any way disclose this document or any of the enclosed information to any person other than employees of addressee to the extent necessary for consultations with authorized personnel of Uber.