Skip to content

Commit

Permalink
Add UI test, add docs, and improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
vishnuravi committed Nov 11, 2024
1 parent 7e70b18 commit b5570ca
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 18 deletions.
8 changes: 8 additions & 0 deletions TemplateApplication.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */; };
56E708352BB06B7100B08F0A /* SpeziLicense in Frameworks */ = {isa = PBXBuildFile; productRef = 56E708342BB06B7100B08F0A /* SpeziLicense */; };
56E7083B2BB06F6F00B08F0A /* SwiftPackageList in Frameworks */ = {isa = PBXBuildFile; productRef = 56E7083A2BB06F6F00B08F0A /* SwiftPackageList */; };
63A315532CE14A9300310EF5 /* LogViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A315522CE14A8E00310EF5 /* LogViewerTests.swift */; };
63A315562CE14C1000310EF5 /* LogManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A315552CE14C0900310EF5 /* LogManagerError.swift */; };
63E851782CE0FFCB005554E7 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */; };
63E8517A2CE0FFDE005554E7 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E851792CE0FFDC005554E7 /* LogLevel.swift */; };
63E8517C2CE0FFF9005554E7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517B2CE0FFF6005554E7 /* LogManager.swift */; };
Expand Down Expand Up @@ -123,6 +125,8 @@
2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = "<group>"; };
2FF53D8C2A8729D600042B76 /* TemplateApplicationStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateApplicationStandard.swift; sourceTree = "<group>"; };
5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributionsTest.swift; sourceTree = "<group>"; };
63A315522CE14A8E00310EF5 /* LogViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerTests.swift; sourceTree = "<group>"; };
63A315552CE14C0900310EF5 /* LogManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManagerError.swift; sourceTree = "<group>"; };
63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLogEntryLog+FormattedLogOutput.swift"; sourceTree = "<group>"; };
63E851792CE0FFDC005554E7 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
63E8517B2CE0FFF6005554E7 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -264,6 +268,7 @@
63E851762CE0FF85005554E7 /* Logging */ = {
isa = PBXGroup;
children = (
63A315552CE14C0900310EF5 /* LogManagerError.swift */,
63E8517F2CE10014005554E7 /* LogsListView.swift */,
63E8517D2CE10005005554E7 /* LogViewer.swift */,
63E8517B2CE0FFF6005554E7 /* LogManager.swift */,
Expand Down Expand Up @@ -327,6 +332,7 @@
653A256A28338800005D4D48 /* TemplateApplicationUITests */ = {
isa = PBXGroup;
children = (
63A315522CE14A8E00310EF5 /* LogViewerTests.swift */,
2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */,
653A256B28338800005D4D48 /* SchedulerTests.swift */,
2F4E23862989DB360013F3D9 /* ContactsTests.swift */,
Expand Down Expand Up @@ -560,6 +566,7 @@
2FC975A82978F11A00BA99FE /* HomeView.swift in Sources */,
63E851782CE0FFCB005554E7 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */,
2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */,
63A315562CE14C1000310EF5 /* LogManagerError.swift in Sources */,
63E851802CE10016005554E7 /* LogsListView.swift in Sources */,
A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */,
A9A3DCC82C75CBBD00FC9B69 /* FirebaseConfiguration.swift in Sources */,
Expand Down Expand Up @@ -596,6 +603,7 @@
buildActionMask = 2147483647;
files = (
5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */,
63A315532CE14A9300310EF5 /* LogViewerTests.swift in Sources */,
2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */,
2F4E237E2989A2FE0013F3D9 /* OnboardingTests.swift in Sources */,
653A256C28338800005D4D48 /* SchedulerTests.swift in Sources */,
Expand Down
33 changes: 28 additions & 5 deletions TemplateApplication/Logging/LogManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,44 @@ import OSLog
import Spezi
import SwiftUI

/// Manages log entries within the application using `OSLogStore`, allowing querying
/// based on date ranges and log levels.
class LogManager {
/// Reference to the `OSLogStore`, which provides access to system logs.
private let store: OSLogStore?

init() {
self.store = try? OSLogStore(scope: .currentProcessIdentifier)
/// Initializes the `LogManager` and attempts to set up the `OSLogStore` with
/// a scope limited to the current process identifier.
///
/// - Throws: An error if the `OSLogStore` cannot be initialized.
init() throws {
do {
self.store = try OSLogStore(scope: .currentProcessIdentifier)
} catch {
throw error
}
}

/// Queries logs within a specified date range and optional log level.
///
/// - Parameters:
/// - startDate: The start date from which logs should be queried.
/// - endDate: An optional end date up to which logs should be queried.
/// - logLevel: An optional log level filter, returning only entries of this level if specified.
/// - Returns: An array of `OSLogEntryLog` entries that match the specified criteria.
/// - Throws: `LogManagerError.invalidLogStore` if `OSLogStore` is unavailable, or
/// `LogManagerError.invalidBundleIdentifier` if the bundle identifier cannot be retrieved.
func query(
startDate: Date,
endDate: Date? = nil,
logLevel: OSLogEntryLog.Level? = nil
) throws -> [OSLogEntryLog] {
guard let store,
let bundleIdentifier = Bundle.main.bundleIdentifier else {
return []
guard let store else {
throw LogManagerError.invalidLogStore
}

guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
throw LogManagerError.invalidBundleIdentifier
}

let position = store.position(date: startDate)
Expand Down
25 changes: 25 additions & 0 deletions TemplateApplication/Logging/LogManagerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// This source file is part of the Stanford Spezi Template Application open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

enum LogManagerError: Error {
/// Throw when the log store is invalid
case invalidLogStore
/// Throw when the bundle identifier is invalid
case invalidBundleIdentifier
}

extension LogManagerError: CustomStringConvertible {
public var description: String {
switch self {
case .invalidLogStore:
return "The OSLogStore is invalid."
case .invalidBundleIdentifier:
return "The bundle identifier is invalid."
}
}
}
54 changes: 41 additions & 13 deletions TemplateApplication/Logging/LogViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import OSLog
import Spezi
import SwiftUI


/// A SwiftUI view that displays logs retrieved from `LogManager`.
/// Allows users to filter logs by date range and log level, view them in a list, and share the output.
struct LogViewer: View {
let manager = LogManager()
private let manager: LogManager?

@State private var startDate: Date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
@State private var endDate = Date()
Expand Down Expand Up @@ -64,15 +65,20 @@ struct LogViewer: View {
queryLogs()
}
.toolbar {
if !logs.isEmpty {
ShareLink(
item: logs.formattedLogOutput(),
preview: SharePreview(
"LOGS_SHARE_PREVIEW_TITLE",
image: Image(systemName: "doc.text") // swiftlint:disable:this accessibility_label_for_image
)
) {
Image(systemName: "square.and.arrow.up") // swiftlint:disable:this accessibility_label_for_image
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button(action: queryLogs) {
Image(systemName: "arrow.clockwise") // swiftlint:disable:this accessibility_label_for_image
}
if !logs.isEmpty {
ShareLink(
item: logs.formattedLogOutput(),
preview: SharePreview(
"LOGS_SHARE_PREVIEW_TITLE",
image: Image(systemName: "doc.text") // swiftlint:disable:this accessibility_label_for_image
)
) {
Image(systemName: "square.and.arrow.up") // swiftlint:disable:this accessibility_label_for_image
}
}
}
}
Expand All @@ -81,7 +87,22 @@ struct LogViewer: View {
}
}

init() {
do {
self.manager = try LogManager()
} catch {
self.manager = nil
displayError(message: error.localizedDescription)
}
}

/// Queries logs based on the selected date range and log level.
/// Cancels any existing query, updates `isLoading` state, and performs a new asynchronous query.
private func queryLogs() {
guard let manager else {
return
}

/// Cancel any existing query task
queryTask?.cancel()

Expand Down Expand Up @@ -109,9 +130,16 @@ struct LogViewer: View {
isLoading = false
}
} catch {
errorMessage = error.localizedDescription
self.showingAlert = true
displayError(message: error.localizedDescription)
}
}
}

/// Displays an error message in an alert if a query fails.
///
/// - Parameter message: The error message to display.
private func displayError(message: String) {
errorMessage = message
showingAlert = true
}
}
6 changes: 6 additions & 0 deletions TemplateApplication/Logging/LogsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ struct LogsListView: View {
.background(LogLevel(from: entry.level).color)
.cornerRadius(4)
}
Text(entry.subsystem)
.font(.caption)
.fontWeight(.semibold)
.padding(2)
.background(Color(.systemGray5))
.cornerRadius(4)
Text(entry.composedMessage)
}
}
Expand Down
45 changes: 45 additions & 0 deletions TemplateApplicationUITests/LogViewerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// This source file is part of the Stanford Spezi Template Application open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

import OSLog
import XCTest
import XCTestExtensions
import XCTHealthKit
import XCTSpeziAccount
import XCTSpeziNotifications

class LogViewerTests: XCTestCase {
@MainActor
override func setUp() async throws {
continueAfterFailure = false

let app = XCUIApplication()
app.launchArguments = ["--skipOnboarding"]
app.launch()
}


@MainActor
func testLogViewer() throws {
let app = XCUIApplication()

XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0))

sleep(2)

XCTAssertTrue(app.navigationBars.buttons["Your Account"].waitForExistence(timeout: 6.0))
app.navigationBars.buttons["Your Account"].tap()

XCTAssertTrue(app.buttons["View Logs"].waitForExistence(timeout: 2.0))
app.buttons["View Logs"].tap()

XCTAssertTrue(app.staticTexts["Log Viewer"].waitForExistence(timeout: 2.0))

XCTAssertTrue(app.staticTexts["No Logs Available"].waitForExistence(timeout: 5.0))
}
}

0 comments on commit b5570ca

Please sign in to comment.