Skip to content

Commit

Permalink
Smart Insights (#113)
Browse files Browse the repository at this point in the history
* Wrap words for better readability

* Set minimum OS to iOS 13

* Fix emoji parsing in HTML

* Add a smart insights section

* Getting closer to smart insights

* Created a structure for providing smart insights

* Fix tests

* Add smart insights tests

* Added tests for custom insights adding

* Update readme

* Add extra documentation

* Fix SwiftLint warnings

* Add test for unique insights

* Revert unneeded UserDefaultsReporter change

* Fix linting feedback

* Remove todo

* Fix CI

* Small change to trigger a new CI run

* Update README.md

Co-authored-by: Kevin Renskers <[email protected]>

* Update README.md

Co-authored-by: Kevin Renskers <[email protected]>

* Remove old reports before running

* Remove optional array

Co-authored-by: Kevin Renskers <[email protected]>
  • Loading branch information
AvdLee and kevinrenskers authored Feb 16, 2022
1 parent 0aa85f4 commit 5c6c587
Show file tree
Hide file tree
Showing 37 changed files with 551 additions and 79 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ playground.xcworkspace
# Package.pins
# Package.resolved
.build/
.spm-build/

# CocoaPods
#
Expand Down
4 changes: 3 additions & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/Diagnostics.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
language = "en"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
Expand Down
Binary file modified Assets/example_report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Assets/smart-insights.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 0 additions & 4 deletions Dangerfile.swift

This file was deleted.

50 changes: 43 additions & 7 deletions DiagnosticsTests/DiagnosticsReporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ final class DiagnosticsReporterTests: XCTestCase {
try! DiagnosticsLogger.setup()
}

override class func tearDown() {
override func tearDown() {
try! DiagnosticsLogger.standard.deleteLogs()
super.tearDown()
}

/// It should correctly generate HTML from the reporters.
func testHTMLGeneration() {
let diagnosticsChapter = DiagnosticsChapter(title: UUID().uuidString, diagnostics: UUID().uuidString)
MockedReporter.diagnosticsChapter = diagnosticsChapter
let reporters = [MockedReporter.self]
var reporter = MockedReporter()
reporter.diagnosticsChapter = diagnosticsChapter
let reporters = [reporter]
let report = DiagnosticsReporter.create(using: reporters)
let html = String(data: report.data, encoding: .utf8)!

Expand All @@ -45,13 +46,36 @@ final class DiagnosticsReporterTests: XCTestCase {
/// It should filter using passed filters.
func testFilters() {
let keyToFilter = UUID().uuidString
MockedReport.diagnostics = [keyToFilter: UUID().uuidString]
let report = DiagnosticsReporter.create(using: [MockedReport.self], filters: [MockedFilter.self])
let mockedReport = MockedReport(diagnostics: [keyToFilter: UUID().uuidString])
let report = DiagnosticsReporter.create(using: [mockedReport], filters: [MockedFilter.self])
let html = String(data: report.data, encoding: .utf8)!
XCTAssertFalse(html.contains(keyToFilter))
XCTAssertTrue(html.contains("FILTERED"))
}

func testWithoutProvidingSmartInsightsProvider() {
let mockedReport = MockedReport(diagnostics: ["key": UUID().uuidString])
let report = DiagnosticsReporter.create(using: [mockedReport, SmartInsightsReporter()], filters: [MockedFilter.self], smartInsightsProvider: nil)
let html = String(data: report.data, encoding: .utf8)!
XCTAssertTrue(html.contains("Smart Insights"), "Default insights should still be added")
}

func testWithSmartInsightsProviderReturningNoExtraInsights() {
let mockedReport = MockedReport(diagnostics: ["key": UUID().uuidString])
let report = DiagnosticsReporter.create(using: [mockedReport, SmartInsightsReporter()], filters: [MockedFilter.self], smartInsightsProvider: MockedInsightsProvider(insightToReturn: nil))
let html = String(data: report.data, encoding: .utf8)!
XCTAssertTrue(html.contains("Smart Insights"), "Default insights should still be added")
}

func testWithSmartInsightsProviderReturningExtraInsights() {
let mockedReport = MockedReport(diagnostics: ["key": UUID().uuidString])
let insightToReturn = SmartInsight(name: UUID().uuidString, result: .success(message: UUID().uuidString))
let report = DiagnosticsReporter.create(using: [mockedReport, SmartInsightsReporter()], filters: [MockedFilter.self], smartInsightsProvider: MockedInsightsProvider(insightToReturn: insightToReturn))
let html = String(data: report.data, encoding: .utf8)!
XCTAssertTrue(html.contains(insightToReturn.name))
XCTAssertTrue(html.contains(insightToReturn.result.message))
}

/// It should correctly generate the header.
func testHeaderGeneration() {
let report = DiagnosticsReporter.create(using: [])
Expand All @@ -65,8 +89,8 @@ final class DiagnosticsReporterTests: XCTestCase {
}

struct MockedReport: DiagnosticsReporting {
static var diagnostics: Diagnostics = [:]
static func report() -> DiagnosticsChapter {
var diagnostics: Diagnostics = [:]
func report() -> DiagnosticsChapter {
return DiagnosticsChapter(title: UUID().uuidString, diagnostics: diagnostics)
}
}
Expand All @@ -76,3 +100,15 @@ struct MockedFilter: DiagnosticsReportFilter {
return "FILTERED"
}
}

struct MockedInsightsProvider: SmartInsightsProviding {
let insightToReturn: SmartInsightProviding?

func smartInsights(for chapter: DiagnosticsChapter) -> [SmartInsightProviding] {
guard let insightToReturn = insightToReturn else {
return []
}

return [insightToReturn]
}
}
4 changes: 2 additions & 2 deletions DiagnosticsTests/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import Diagnostics

struct MockedReporter: DiagnosticsReporting {

static var diagnosticsChapter: DiagnosticsChapter!
var diagnosticsChapter: DiagnosticsChapter!

static func report() -> DiagnosticsChapter {
func report() -> DiagnosticsChapter {
return diagnosticsChapter
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class AppSystemMetadataReporterTests: XCTestCase {

/// It should correctly add the metadata.
func testMetadata() {
let metadata = AppSystemMetadataReporter.report().diagnostics as! [String: String]
let metadata = AppSystemMetadataReporter().report().diagnostics as! [String: String]

XCTAssertEqual(metadata[AppSystemMetadataReporter.MetadataKey.appName.rawValue], Bundle.appName)
XCTAssertEqual(metadata[AppSystemMetadataReporter.MetadataKey.appVersion.rawValue], "\(Bundle.appVersion) (\(Bundle.appBuildNumber))")
Expand Down
7 changes: 4 additions & 3 deletions DiagnosticsTests/Reporters/GeneralInfoReporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ final class GeneralInfoReporterTests: XCTestCase {

/// It should include the title in the report.
func testTitle() {
XCTAssertEqual(GeneralInfoReporter.report().title, "Information")
XCTAssertEqual(GeneralInfoReporter().report().title, "Information")
}

func testDescription() {
let diagnostics = GeneralInfoReporter.report().diagnostics as! String
XCTAssertEqual(diagnostics, GeneralInfoReporter.description)
let reporter = GeneralInfoReporter()
let diagnostics = reporter.report().diagnostics as! String
XCTAssertEqual(diagnostics, reporter.description)
}

}
19 changes: 13 additions & 6 deletions DiagnosticsTests/Reporters/LogsReporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,39 @@ final class LogsReporterTests: XCTestCase {
try DiagnosticsLogger.standard.deleteLogs()
try super.tearDownWithError()
}

/// It should show logged messages.
func testMessagesLog() {
func testMessagesLog() throws {
let message = UUID().uuidString
DiagnosticsLogger.log(message: message)
let diagnostics = LogsReporter.report().diagnostics as! String
let diagnostics = LogsReporter().report().diagnostics as! String
XCTAssertTrue(diagnostics.contains(message), "Diagnostics is \(diagnostics)")
XCTAssertEqual(diagnostics.debugLogs.count, 1)
let debugLog = try XCTUnwrap(diagnostics.debugLogs.first)
XCTAssertTrue(debugLog.contains("<span class=\"log-prefix\">LogsReporterTests.swift:L27</span>"), "Prefix should be added")
XCTAssertTrue(debugLog.contains("<span class=\"log-message\">\(message)</span>"), "Log message should be added")
}

/// It should show errors.
func testErrorLog() {
func testErrorLog() throws {
enum Error: Swift.Error {
case testCase
}

DiagnosticsLogger.log(error: Error.testCase)
let diagnostics = LogsReporter.report().diagnostics as! String
let diagnostics = LogsReporter().report().diagnostics as! String
XCTAssertTrue(diagnostics.contains("testCase"))
XCTAssertEqual(diagnostics.errorLogs.count, 1)
let errorLog = try XCTUnwrap(diagnostics.errorLogs.first)
XCTAssertTrue(errorLog.contains("<span class=\"log-message\">ERROR: testCase"))
}

/// It should reverse the order of sessions to have the most recent session on top.
func testReverseSessions() throws {
DiagnosticsLogger.log(message: "first")
DiagnosticsLogger.standard.startNewSession()
DiagnosticsLogger.log(message: "second")
let diagnostics = LogsReporter.report().diagnostics as! String
let diagnostics = LogsReporter().report().diagnostics as! String
let firstIndex = try XCTUnwrap(diagnostics.range(of: "first")?.lowerBound)
let secondIndex = try XCTUnwrap(diagnostics.range(of: "second")?.lowerBound)
XCTAssertTrue(firstIndex > secondIndex)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// DeviceStorageInsightTests.swift
// Diagnostics
//
// Created by Antoine van der Lee on 10/02/2022.
// Copyright © 2019 WeTransfer. All rights reserved.
//

import XCTest
@testable import Diagnostics

final class DeviceStorageInsightTests: XCTestCase {

func testLowOnStorage() {
let insight = DeviceStorageInsight(freeDiskSpace: 800 * 1000 * 1000, totalDiskSpace: "100GB")
XCTAssertEqual(insight.result, .warn(message: "The user is low on storage (800 MB of 100GB left)"))
}

func testEnoughStorage() {
let insight = DeviceStorageInsight(freeDiskSpace: 8000 * 1000 * 1000, totalDiskSpace: "100GB")
XCTAssertEqual(insight.result, .success(message: "The user has enough storage (8 GB of 100GB left)"))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// SmartInsightsReporterTests.swift
// Diagnostics
//
// Created by Antoine van der Lee on 10/02/2022.
// Copyright © 2019 WeTransfer. All rights reserved.
//

import XCTest
@testable import Diagnostics

final class SmartInsightsReporterTests: XCTestCase {

func testSmartInsightsChapter() throws {
let reporter = SmartInsightsReporter()
let chapter = reporter.report()
XCTAssertEqual(chapter.title, "Smart Insights")
let insightsDictionary = try XCTUnwrap(chapter.diagnostics as? [String: String])
XCTAssertFalse(insightsDictionary.isEmpty)
}

func testRemovingDuplicateInsights() throws {
var reporter = SmartInsightsReporter()
let insight = SmartInsight(name: UUID().uuidString, result: .success(message: UUID().uuidString))

/// Remove default insights to make this test independent.
reporter.insights.removeAll()

reporter.insights.append(contentsOf: [insight, insight, insight])

let chapter = reporter.report()
XCTAssertEqual(chapter.title, "Smart Insights")
let insightsDictionary = try XCTUnwrap(chapter.diagnostics as? [String: String])
XCTAssertEqual(insightsDictionary.count, 1, "It should only have one of the custom insights")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// UpdateAvailableInsightTests.swift
// Diagnostics
//
// Created by Antoine van der Lee on 10/02/2022.
// Copyright © 2019 WeTransfer. All rights reserved.
//

import XCTest
@testable import Diagnostics
import Combine

final class UpdateAvailableInsightTests: XCTestCase {

let exampleError = NSError(domain: UUID().uuidString, code: -1, userInfo: nil)
let sampleBundleIdentifier = "com.wetransfer.example.app"

func testReturningNilIfNoBundleIdentifier() {
XCTAssertNil(UpdateAvailableInsight(bundleIdentifier: nil))
}

func testReturningNilIfNoAppMetadataAvailable() {
let publisher: AnyPublisher<AppMetadataResults, Error> = Fail(error: exampleError).eraseToAnyPublisher()
let insight = UpdateAvailableInsight(bundleIdentifier: sampleBundleIdentifier, appMetadataPublisher: publisher)
XCTAssertNil(insight)
}

func testUserIsOnTheSameVersion() {
let appMetadata = AppMetadataResults(results: [.init(version: "1.0.0")])
let publisher: AnyPublisher<AppMetadataResults, Error> = Just(appMetadata)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()

let insight = UpdateAvailableInsight(bundleIdentifier: sampleBundleIdentifier, currentVersion: "1.0.0", appMetadataPublisher: publisher)
XCTAssertEqual(insight?.result, .success(message: "The user is using the latest app version 1.0.0"))
}

func testUserIsOnANewerVersion() {
let appMetadata = AppMetadataResults(results: [.init(version: "1.0.0")])
let publisher: AnyPublisher<AppMetadataResults, Error> = Just(appMetadata)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()

let insight = UpdateAvailableInsight(bundleIdentifier: sampleBundleIdentifier, currentVersion: "2.0.0", appMetadataPublisher: publisher)
XCTAssertEqual(insight?.result, .success(message: "The user is using a newer version 2.0.0"))
}

func testUserIsOnAnOlderVersion() {
let appMetadata = AppMetadataResults(results: [.init(version: "2.0.0")])
let publisher: AnyPublisher<AppMetadataResults, Error> = Just(appMetadata)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()

let insight = UpdateAvailableInsight(bundleIdentifier: sampleBundleIdentifier, currentVersion: "1.0.0", appMetadataPublisher: publisher)
XCTAssertEqual(insight?.result, .warn(message: "The user could update to 2.0.0"))
}
}
2 changes: 1 addition & 1 deletion DiagnosticsTests/Reporters/UserDefaultsReporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class UserDefaultsReporterTests: XCTestCase {
func testReportUserDefaults() {
let expectedValue = UUID().uuidString
UserDefaults.standard.set(expectedValue, forKey: "test_key")
let diagnostics = UserDefaultsReporter.report().diagnostics as! [String: Any]
let diagnostics = UserDefaultsReporter().report().diagnostics as! [String: Any]
XCTAssertEqual(diagnostics["test_key"] as? String, expectedValue)
}

Expand Down
4 changes: 4 additions & 0 deletions Example/Diagnostics-Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
50500100239FA4CB00EADD27 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505000FA239FA4CA00EADD27 /* AppDelegate.swift */; };
50500102239FA4CB00EADD27 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505000FC239FA4CA00EADD27 /* SceneDelegate.swift */; };
509F59CE2397AD0D006AD8D1 /* CustomReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509F59CD2397AD0D006AD8D1 /* CustomReporter.swift */; };
8461D42627B520A200A520DF /* CustomSmartInsights.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8461D42527B520A200A520DF /* CustomSmartInsights.swift */; };
84F575FF26DE67180032BB3A /* Diagnostics in Frameworks */ = {isa = PBXBuildFile; productRef = 84F575FE26DE67180032BB3A /* Diagnostics */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -42,6 +43,7 @@
505000FB239FA4CA00EADD27 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
505000FC239FA4CA00EADD27 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
509F59CD2397AD0D006AD8D1 /* CustomReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomReporter.swift; sourceTree = "<group>"; };
8461D42527B520A200A520DF /* CustomSmartInsights.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSmartInsights.swift; sourceTree = "<group>"; };
84F575FD26DE666B0032BB3A /* Diagnostics */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Diagnostics; path = ..; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -82,6 +84,7 @@
500B277B23953F8C00C304D4 /* ViewController.swift */,
509F59CD2397AD0D006AD8D1 /* CustomReporter.swift */,
505000F1239FA4AB00EADD27 /* CustomFilters.swift */,
8461D42527B520A200A520DF /* CustomSmartInsights.swift */,
);
path = "Diagnostics-Example";
sourceTree = "<group>";
Expand Down Expand Up @@ -183,6 +186,7 @@
buildActionMask = 2147483647;
files = (
50500100239FA4CB00EADD27 /* AppDelegate.swift in Sources */,
8461D42627B520A200A520DF /* CustomSmartInsights.swift in Sources */,
50500102239FA4CB00EADD27 /* SceneDelegate.swift in Sources */,
500B277C23953F8C00C304D4 /* ViewController.swift in Sources */,
509F59CE2397AD0D006AD8D1 /* CustomReporter.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Example/Diagnostics-Example/CustomReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum Session {

/// An example Custom Reporter.
struct CustomReporter: DiagnosticsReporting {
static func report() -> DiagnosticsChapter {
func report() -> DiagnosticsChapter {
let diagnostics: [String: String] = [
"Logged In": Session.isLoggedIn.description
]
Expand Down
25 changes: 25 additions & 0 deletions Example/Diagnostics-Example/CustomSmartInsights.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// CustomSmartInsights.swift
// Diagnostics
//
// Created by Antoine van der Lee on 10/02/2022.
// Copyright © 2019 WeTransfer. All rights reserved.
//

import Foundation
import Diagnostics

struct SmartInsightsProvider: SmartInsightsProviding {
func smartInsights(for chapter: DiagnosticsChapter) -> [SmartInsightProviding]? {
guard let html = chapter.diagnostics as? HTML else { return nil }
if html.errorLogs.contains(where: { $0.contains("AppDelegate.ExampleLocalizedError") }) {
return [
SmartInsight(
name: "Localized data",
result: .warn(message: "An error was found regarding missing localisation.")
)
]
}
return nil
}
}
Loading

0 comments on commit 5c6c587

Please sign in to comment.