Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swap snapshots functionality; #47

Merged
merged 24 commits into from
Jul 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aa3e9e7
Create fbReferenceImageDirMessage log line;
Antondomashnev Jun 23, 2017
f3a8063
Create FBReferenceImageExtractor; Create ApplicationSnapshotTestResul…
Antondomashnev Jun 24, 2017
3c39524
Create BuildCreatorSpec;
Antondomashnev Jun 24, 2017
101b571
Create FBReferenceImageDirectoryURLExtractorSpec;
Antondomashnev Jun 24, 2017
d6d60ab
Working on tests;
Antondomashnev Jun 25, 2017
6a2a9e1
Create spec for SnapshotTestResultSwapperSpec;
Antondomashnev Jun 27, 2017
4b28b4b
Implementing swap functionality;
Antondomashnev Jun 27, 2017
6bf5f31
SnapshotTestResultSwapper covered with tests;
Antondomashnev Jun 28, 2017
a2c2469
Add missing test for SnapshotTestResultSwapper;
Antondomashnev Jun 28, 2017
1728e9d
Adding swap snapshots button;
Antondomashnev Jun 28, 2017
089dc91
Updating tests;
Antondomashnev Jun 29, 2017
113ed28
Fix tests;
Antondomashnev Jun 30, 2017
d2bf4ee
Renames automockable methods according to new template;
Antondomashnev Jun 30, 2017
14f0c3d
Add additional tests;
Antondomashnev Jun 30, 2017
b448587
Add can be swapped property for TestResultDisplayInfo;
Antondomashnev Jun 30, 2017
607e948
Invalidate image cache after swap;
Antondomashnev Jul 1, 2017
3f62932
Reload specific parts of collection view in test results controller;
Antondomashnev Jul 1, 2017
021247d
Show correct UI after performed swap;
Antondomashnev Jul 2, 2017
91961ab
Show error alert if swap was not performed properly;
Antondomashnev Jul 2, 2017
287490a
Update changelog;
Antondomashnev Jul 2, 2017
4282903
Refactor according codebeat feedback;
Antondomashnev Jul 2, 2017
deaa9b4
Finalize codebeat refactoring;
Antondomashnev Jul 2, 2017
02b76e1
Hide swap button in section if nothing to swap;
Antondomashnev Jul 2, 2017
c7a81e6
Small improvement;
Antondomashnev Jul 2, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [#41](https://github.com/Antondomashnev/FBSnapshotsViewer/pull/41): Add additional information for tests - [@antondomashnev](https://github.com/antondomashnev).
* [#44](https://github.com/Antondomashnev/FBSnapshotsViewer/pull/44): Compact view redesign - [@antondomashnev](https://github.com/antondomashnev).
* [#45](https://github.com/Antondomashnev/FBSnapshotsViewer/pull/45): Use Nuke for image loading - [@antondomashnev](https://github.com/antondomashnev).
* [47](https://github.com/Antondomashnev/FBSnapshotsViewer/pull/47): Swap snapshots - [@antondomashnev](https://github.com/antondomashnev).

### 0.5.0 (26.05.2017)

Expand Down
44 changes: 44 additions & 0 deletions FBSnapshotsViewer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions FBSnapshotsViewer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let wireframe = MenuWireframe()
menuUserInterface = wireframe.instantinateMenu(in: NSStatusBar.system(), configuration: cofiguration)
}

func setUpConfiguration() -> Configuration {

// MARK: - Helpers

private func setUpConfiguration() -> Configuration {
let configurationStorage = UserDefaultsConfigurationStorage()
if let configuration = configurationStorage.loadConfiguration() {
print("App already has configuration stored")
Expand Down
16 changes: 16 additions & 0 deletions FBSnapshotsViewer/Extensions/FileManager+Move.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// FileManager+Move.swift
// FBSnapshotsViewer
//
// Created by Anton Domashnev on 02.07.17.
// Copyright © 2017 Anton Domashnev. All rights reserved.
//

import Foundation

extension FileManager {
func moveItem(at fromURL: URL, to toURL: URL) throws {
try self.removeItem(at: toURL)
try self.copyItem(at: fromURL, to: toURL)
}
}
15 changes: 15 additions & 0 deletions FBSnapshotsViewer/Extensions/Nuke+ImageCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Nuke+ImageCache.swift
// FBSnapshotsViewer
//
// Created by Anton Domashnev on 01.07.17.
// Copyright © 2017 Anton Domashnev. All rights reserved.
//

import Nuke

extension Nuke.Cache: ImageCache {
func invalidate() {
self.removeAll()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,14 @@ import KZFileWatchers
typealias ApplicationSnapshotTestResultListenerOutput = (SnapshotTestResult) -> Void

class ApplicationSnapshotTestResultListener {
private var build: Build?
private var readLinesNumber: Int = 0
private var listeningOutput: ApplicationSnapshotTestResultListenerOutput?
private let fileWatcher: KZFileWatchers.FileWatcherProtocol
private let applicationLogReader: ApplicationLogReader
private let snapshotTestResultFactory: SnapshotTestResultFactory
private let applicationNameExtractor: ApplicationNameExtractor
private let fileWatcherUpdateHandler: ApplicationSnapshotTestResultFileWatcherUpdateHandler

init(fileWatcher: KZFileWatchers.FileWatcherProtocol, applicationLogReader: ApplicationLogReader, applicationNameExtractor: ApplicationNameExtractor, snapshotTestResultFactory: SnapshotTestResultFactory = SnapshotTestResultFactory()) {
init(fileWatcher: KZFileWatchers.FileWatcherProtocol,
fileWatcherUpdateHandler: ApplicationSnapshotTestResultFileWatcherUpdateHandler) {
self.fileWatcher = fileWatcher
self.applicationLogReader = applicationLogReader
self.snapshotTestResultFactory = snapshotTestResultFactory
self.applicationNameExtractor = applicationNameExtractor
self.fileWatcherUpdateHandler = fileWatcherUpdateHandler
}

deinit {
Expand All @@ -36,7 +31,12 @@ class ApplicationSnapshotTestResultListener {
listeningOutput = completion
do {
try fileWatcher.start { [weak self] result in
self?.handleFileWatcherUpdate(result: result)
guard let listeningOutput = self?.listeningOutput,
let testResults = self?.fileWatcherUpdateHandler.handleFileWatcherUpdate(result: result),
!testResults.isEmpty else {
return
}
testResults.forEach { listeningOutput($0) }
}
}
catch let error {
Expand All @@ -55,54 +55,8 @@ class ApplicationSnapshotTestResultListener {
reset()
}

// MARK: - Helpers

private func handleFileWatcherUpdate(result: KZFileWatchers.FileWatcher.RefreshResult) {
switch result {
case .noChanges:
return
case let .updated(data):
guard let text = String(data: data, encoding: .utf8), !text.isEmpty else {
assertionFailure("Invalid data reported by KZFileWatchers.FileWatcher.Local")
return
}
do {
try handleFileWatcherUpdate(text: text)
}
catch let error {
assertionFailure("\(error)")
}
}
}

private func handleFileWatcherUpdate(text: String) throws {
guard let listeningOutput = listeningOutput else {
return
}
let logLines = applicationLogReader.readline(of: text, startingFrom: readLinesNumber)
let snapshotTestResults = try logLines.flatMap { logLine -> SnapshotTestResult? in
switch logLine {
case .unknown:
return nil
case .applicationNameMessage:
build = Build(applicationName: try applicationNameExtractor.extractApplicationName(from: logLine))
return nil
default:
guard let build = build else {
assertionFailure("Unexpected snapshot test result line \(logLine) before build information line")
return nil
}
return snapshotTestResultFactory.createSnapshotTestResult(from: logLine, build: build)
}
}
snapshotTestResults.forEach { listeningOutput($0) }
readLinesNumber += logLines.count
}

private func reset() {
readLinesNumber = 0
listeningOutput = nil
build = nil
try? fileWatcher.stop()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// ApplicationSnapshotTestResultFileWatcherUpdateHandler.swift
// FBSnapshotsViewer
//
// Created by Anton Domashnev on 24.06.17.
// Copyright © 2017 Anton Domashnev. All rights reserved.
//

import Foundation
import KZFileWatchers

class ApplicationSnapshotTestResultFileWatcherUpdateHandlerBuilder {
var applicationLogReader: ApplicationLogReader = ApplicationLogReader()
var snapshotTestResultFactory: SnapshotTestResultFactory = SnapshotTestResultFactory()
var applicationNameExtractor: ApplicationNameExtractor = XcodeApplicationNameExtractor()
var fbImageReferenceDirExtractor: FBReferenceImageDirectoryURLExtractor = XcodeFBReferenceImageDirectoryURLExtractor()
var buildCreator: BuildCreator = BuildCreator()

typealias BuilderClojure = (ApplicationSnapshotTestResultFileWatcherUpdateHandlerBuilder) -> Void

init(clojure: BuilderClojure) {
clojure(self)
}
}

class ApplicationSnapshotTestResultFileWatcherUpdateHandler {
private let applicationLogReader: ApplicationLogReader
private let snapshotTestResultFactory: SnapshotTestResultFactory
private let applicationNameExtractor: ApplicationNameExtractor
private let fbImageReferenceDirExtractor: FBReferenceImageDirectoryURLExtractor
private let buildCreator: BuildCreator
private var readLinesNumber: Int = 0

init(builder: ApplicationSnapshotTestResultFileWatcherUpdateHandlerBuilder = ApplicationSnapshotTestResultFileWatcherUpdateHandlerBuilder(clojure: { _ in })) {
self.applicationLogReader = builder.applicationLogReader
self.snapshotTestResultFactory = builder.snapshotTestResultFactory
self.applicationNameExtractor = builder.applicationNameExtractor
self.buildCreator = builder.buildCreator
self.fbImageReferenceDirExtractor = builder.fbImageReferenceDirExtractor
}

// MARK: - Interface

@discardableResult func handleFileWatcherUpdate(result: KZFileWatchers.FileWatcher.RefreshResult) -> [SnapshotTestResult] {
switch result {
case .noChanges:
return []
case let .updated(data):
guard let text = String(data: data, encoding: .utf8), !text.isEmpty else {
assertionFailure("Invalid data reported by KZFileWatchers.FileWatcher.Local")
return []
}
do {
return try handleFileWatcherUpdate(text: text) ?? []
}
catch let error {
assertionFailure("\(error)")
return []
}
}
}

// MARK: - Helpers

private func logLinesFlatMap() -> (ApplicationLogLine) throws -> SnapshotTestResult? {
return { [weak self] logLine -> SnapshotTestResult? in
guard let strongSelf = self else {
return nil
}
switch logLine {
case .unknown:
return nil
case .fbReferenceImageDirMessage:
strongSelf.buildCreator.fbReferenceImageDirectoryURL = try strongSelf.fbImageReferenceDirExtractor.extractImageDirectoryURL(from: logLine)
return nil
case .applicationNameMessage:
strongSelf.buildCreator.applicationName = try strongSelf.applicationNameExtractor.extractApplicationName(from: logLine)
strongSelf.buildCreator.date = Date()
return nil
default:
guard let build = strongSelf.buildCreator.createBuild() else {
assertionFailure("Unexpected snapshot test result line \(logLine) before build information line")
return nil
}
return strongSelf.snapshotTestResultFactory.createSnapshotTestResult(from: logLine, build: build)
}
}
}

private func handleFileWatcherUpdate(text: String) throws -> [SnapshotTestResult]? {
let logLines = applicationLogReader.readline(of: text, startingFrom: readLinesNumber)
let snapshotTestResults = try logLines.flatMap(logLinesFlatMap())
readLinesNumber += logLines.count
return snapshotTestResults
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,26 @@ import KZFileWatchers

class ApplicationSnapshotTestResultListenerFactory {
private let applicationNameExtractorFactory: ApplicationNameExtractorFactory
private let fbReferenceImageDirExtractorFactory: FBReferenceImageDirectoryURLExtractorFactory

// MARK: - Interface

init(applicationNameExtractorFactory: ApplicationNameExtractorFactory = ApplicationNameExtractorFactory()) {
init(applicationNameExtractorFactory: ApplicationNameExtractorFactory = ApplicationNameExtractorFactory(), fbReferenceImageDirExtractorFactory: FBReferenceImageDirectoryURLExtractorFactory = FBReferenceImageDirectoryURLExtractorFactory()) {
self.applicationNameExtractorFactory = applicationNameExtractorFactory
self.fbReferenceImageDirExtractorFactory = fbReferenceImageDirExtractorFactory
}

func applicationSnapshotTestResultListener(forLogFileAt path: String, configuration: Configuration = Configuration.default()) -> ApplicationSnapshotTestResultListener {
let fileWatcher = KZFileWatchers.FileWatcher.Local(path: path)
let reader = ApplicationLogReader(configuration: configuration)
let applicationNameExtractor = applicationNameExtractorFactory.applicationNameExtractor(for: configuration)
return ApplicationSnapshotTestResultListener(fileWatcher: fileWatcher, applicationLogReader: reader, applicationNameExtractor: applicationNameExtractor)
let imageDirectoryExtractor = fbReferenceImageDirExtractorFactory.fbReferenceImageDirectoryURLExtractor(for: configuration)
let builder = ApplicationSnapshotTestResultFileWatcherUpdateHandlerBuilder {
$0.applicationLogReader = reader
$0.applicationNameExtractor = applicationNameExtractor
$0.fbImageReferenceDirExtractor = imageDirectoryExtractor
}
let applicationSnapshotTestResultFileWatcherUpdateHandler = ApplicationSnapshotTestResultFileWatcherUpdateHandler(builder: builder)
return ApplicationSnapshotTestResultListener(fileWatcher: fileWatcher, fileWatcherUpdateHandler: applicationSnapshotTestResultFileWatcherUpdateHandler)
}
}
23 changes: 23 additions & 0 deletions FBSnapshotsViewer/Managers/BuildCreator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// BuildCreator.swift
// FBSnapshotsViewer
//
// Created by Anton Domashnev on 24.06.17.
// Copyright © 2017 Anton Domashnev. All rights reserved.
//

import Foundation

class BuildCreator {
var date: Date?
var applicationName: String?
var fbReferenceImageDirectoryURL: URL?

func createBuild() -> Build? {
guard let date = date, let applicationName = applicationName, let fbReferenceImageDirectoryURL = fbReferenceImageDirectoryURL else {
print("Can not create a build if not all require properties are initialized:\ndate: \(String(describing: self.date))\napplicationName: \(String(describing: self.applicationName))\nfbReferenceImageDirectoryURL: \(String(describing: self.fbReferenceImageDirectoryURL))")
return nil
}
return Build(date: date, applicationName: applicationName, fbReferenceImageDirectoryURL: fbReferenceImageDirectoryURL)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// FBReferenceImageDirectoryURLExtractor.swift
// FBSnapshotsViewer
//
// Created by Anton Domashnev on 24.06.17.
// Copyright © 2017 Anton Domashnev. All rights reserved.
//

import Foundation

enum FBReferenceImageDirectoryURLExtractorError: Error {
case unexpectedLogLine(message: String)
}

protocol FBReferenceImageDirectoryURLExtractor: AutoMockable {
func extractImageDirectoryURL(from logLine: ApplicationLogLine) throws -> URL
}

class FBReferenceImageDirectoryURLExtractorFactory {
func fbReferenceImageDirectoryURLExtractor(for configuration: Configuration) -> FBReferenceImageDirectoryURLExtractor {
switch configuration.derivedDataFolder {
case .xcodeCustom,
.xcodeDefault:
return XcodeFBReferenceImageDirectoryURLExtractor()
case .appcode:
return AppCodeFBReferenceImageDirectoryURLExtractor()
}
}
}

class FBReferenceImageDirectoryURLExtractorHelper {
static func buildReferenceImagePathURL(with path: String) -> URL {
// ios-snapshots adds a suffix for the reference dir depending on the acrhitecture. For now we support only 64 bit
return URL(fileURLWithPath: path + "_64", isDirectory: true)
}

static func extractLine(from logLine: ApplicationLogLine) throws -> String {
guard case let .fbReferenceImageDirMessage(line) = logLine,
line.contains("/") else {
throw FBReferenceImageDirectoryURLExtractorError.unexpectedLogLine(message: "Unexpected log line given: \(logLine). Expected .fbReferenceImageDirMessage")
}
return line
}

static func extractImageDirectoryURL(from logLine: ApplicationLogLine, componentsBuilder: (String) -> [String]?, expectedLogLineExample: String) throws -> URL {
let line = try extractLine(from: logLine)
guard let components = componentsBuilder(line), components.count == 3 else {
throw FBReferenceImageDirectoryURLExtractorError.unexpectedLogLine(message: "Unexpected log line given: \(logLine). Expected the following format: \(expectedLogLineExample)")
}
return buildReferenceImagePathURL(with: components[1])
}
}

class XcodeFBReferenceImageDirectoryURLExtractor: FBReferenceImageDirectoryURLExtractor {
func extractImageDirectoryURL(from logLine: ApplicationLogLine) throws -> URL {
let componentsBuilder: (String) -> [String]? = { $0.components(separatedBy: " = ").last?.components(separatedBy: "\"") }
return try FBReferenceImageDirectoryURLExtractorHelper.extractImageDirectoryURL(from: logLine, componentsBuilder: componentsBuilder, expectedLogLineExample: "\"FB_REFERENCE_IMAGE_DIR\" = \"/Users/antondomashnev/Work/FBSnapshotsViewerExample/FBSnapshotsViewerExampleTests/ReferenceImages\";")
}
}

class AppCodeFBReferenceImageDirectoryURLExtractor: FBReferenceImageDirectoryURLExtractor {
func extractImageDirectoryURL(from logLine: ApplicationLogLine) throws -> URL {
let componentsBuilder: (String) -> [String]? = { $0.components(separatedBy: "value=").last?.components(separatedBy: "\"") }
return try FBReferenceImageDirectoryURLExtractorHelper.extractImageDirectoryURL(from: logLine, componentsBuilder: componentsBuilder, expectedLogLineExample: "<env name=\"FB_REFERENCE_IMAGE_DIR\" value=\"/Users/antondomashnev/Work/FBSnapshotsViewerExample/FBSnapshotsViewerExampleTests/ReferenceImages\"/>")
}
}