Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Sources/CLI/cache/DocumentationCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

#if DEBUG
protocol DocumentationCache {
var currentDiskUsage: Int { get }

func removeAllCachedResponses()
}

extension URLCache: DocumentationCache {}
#else
typealias DocumentationCache = URLCache
#endif
50 changes: 50 additions & 0 deletions Sources/CLI/cmd/cache/CacheCleanCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import ArgumentParser
import Logging
@preconcurrency import SentrySwift

struct CacheCleanCommand: ParsableCommand {
private static let logger = Logger(
label: "com.techprimate.apple-docs.cache-clean"
)

static let configuration = CommandConfiguration(
commandName: "clean",
abstract: "Clear cached Apple documentation."
)

mutating func run() throws {
if SentrySDK.isEnabled {
let context = SentryCommandContext.cacheClean
let transaction = SentrySDK.startTransaction(
name: context.transactionName,
operation: "console.command",
bindToScope: true
)
for (key, value) in context.attributes {
transaction.setData(value: value, key: key)
}
SentrySDK.configureScope { scope in
scope.setContext(value: context.attributes, key: "cli")
}
let breadcrumb = Breadcrumb(
level: .info,
category: SentryConfiguration.breadcrumbCategory
)
breadcrumb.type = "user"
breadcrumb.message = "CLI command invoked"
for (key, value) in context.attributes {
breadcrumb.setData(value: value, key: key)
}
SentrySDK.addBreadcrumb(breadcrumb)
Self.logger.info(
"CLI command started",
metadata: context.logMetadata
)
}

let result = CacheCleanCommandRunner(
cache: Dependencies.documentationCache
).run()
print(result.output)
}
}
34 changes: 34 additions & 0 deletions Sources/CLI/cmd/cache/CacheCleanCommandRunner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

struct CacheCleanCommandRunner {
struct Result {
let clearedByteCount: Int?
let output: String
}

private let cache: DocumentationCache?

init(cache: DocumentationCache?) {
self.cache = cache
}

func run() -> Result {
guard let cache else {
return Result(
clearedByteCount: nil,
output: "Apple documentation cache is unavailable."
)
}

let clearedByteCount = cache.currentDiskUsage
cache.removeAllCachedResponses()
let formattedByteCount = ByteCountFormatter.string(
fromByteCount: Int64(clearedByteCount),
countStyle: .file
)
return Result(
clearedByteCount: clearedByteCount,
output: "Cleared \(formattedByteCount) of cached Apple documentation."
)
}
}
9 changes: 9 additions & 0 deletions Sources/CLI/cmd/cache/CacheCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ArgumentParser

struct CacheCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "cache",
abstract: "Manage cached Apple documentation.",
subcommands: [CacheCleanCommand.self]
)
}
1 change: 1 addition & 0 deletions Sources/CLI/main/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct CLI: AsyncParsableCommand {
subcommands: [
TypesCommand.self,
TechnologiesCommand.self,
CacheCommand.self,
AgentCommand.self,
]
)
Expand Down
33 changes: 32 additions & 1 deletion Sources/CLI/main/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
import Foundation

enum Dependencies {
static let httpDataTransport = URLSession.shared
static let httpCache: URLCache? = {
guard
let cachesDirectory = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
).first
else {
return nil
}
let cacheDirectory = cachesDirectory.appendingPathComponent(
"com.techprimate.apple-docs",
isDirectory: true
)
return URLCache(
memoryCapacity: 16_000_000,
diskCapacity: 1_000_000_000,
directory: cacheDirectory
)
}()

static let httpDataTransport: URLSession = {
let configuration = URLSessionConfiguration.default
if let httpCache {
configuration.urlCache = httpCache
}
return URLSession(configuration: configuration)
}()

static var documentationCache: URLCache? {
httpDataTransport.configuration.urlCache
}

static let documentationClient = DefaultAppleDocumentationClient(
dependencies: httpDataTransport
)
Expand Down
6 changes: 6 additions & 0 deletions Sources/CLI/telemetry/SentryCommandContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ struct SentryCommandContext: Equatable, Sendable {
let technology: String?
let typeName: String?

static let cacheClean = SentryCommandContext(
command: "cache.clean",
outputJSON: nil,
technology: nil,
typeName: nil
)
static let agentSkillsGet = SentryCommandContext(
command: "agent.skills.get",
outputJSON: nil,
Expand Down
50 changes: 50 additions & 0 deletions Tests/CLITests/cmd/cache/CacheCleanCommandRunnerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Testing

@testable import CLI

@Suite("Cache clean command runner")
struct CacheCleanCommandRunnerTests {
@Test("clears cached responses and reports their disk usage")
func clearsCache() {
// -- Arrange --
let cache = TestDocumentationCache(currentDiskUsage: 5_000_000)
let runner = CacheCleanCommandRunner(cache: cache)

// -- Act --
let result = runner.run()

// -- Assert --
#expect(cache.isEmpty)
#expect(result.clearedByteCount == 5_000_000)
#expect(result.output == "Cleared 5 MB of cached Apple documentation.")
}
Comment thread
philprime marked this conversation as resolved.

@Test("reports when no cache is available")
func reportsUnavailableCache() {
// -- Arrange --
let runner = CacheCleanCommandRunner(cache: nil)

// -- Act --
let result = runner.run()

// -- Assert --
#expect(result.clearedByteCount == nil)
#expect(result.output == "Apple documentation cache is unavailable.")
}
}

private final class TestDocumentationCache: DocumentationCache {
private(set) var currentDiskUsage: Int

var isEmpty: Bool {
currentDiskUsage == 0
}

init(currentDiskUsage: Int) {
self.currentDiskUsage = currentDiskUsage
}

func removeAllCachedResponses() {
currentDiskUsage = 0
}
}
18 changes: 18 additions & 0 deletions Tests/CLITests/cmd/cache/CacheCleanCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Testing

@testable import CLI

@Suite("Cache clean command")
struct CacheCleanCommandTests {
@Test("registers the cache clean command hierarchy")
func parsesCacheCleanCommand() throws {
// -- Arrange --
let arguments = ["cache", "clean"]

// -- Act --
let command = try CLI.parseAsRoot(arguments)

// -- Assert --
#expect(command is CacheCleanCommand)
}
}
22 changes: 22 additions & 0 deletions Tests/CLITests/main/DependenciesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import Testing

@testable import CLI

@Suite("Dependencies")
struct DependenciesTests {
@Test("uses a dedicated large documentation cache")
func usesDedicatedDocumentationCache() throws {
// -- Arrange --
let session = Dependencies.httpDataTransport

// -- Act --
let cache = try #require(session.configuration.urlCache)
let documentationCache = try #require(Dependencies.documentationCache)

// -- Assert --
#expect(session !== URLSession.shared)
#expect(cache === documentationCache)
#expect(cache.diskCapacity == 1_000_000_000)
}
}
18 changes: 18 additions & 0 deletions Tests/CLITests/telemetry/SentryCommandContextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ struct SentryCommandContextTests {
#expect(context.logMetadata.keys.sorted() == expectedKeys)
}

@Test("uses only the command name for cache clean telemetry")
func includesCacheCleanCommand() {
// -- Arrange --
let expectedKeys = ["cli.command"]

// -- Act --
let context = SentryCommandContext.cacheClean

// -- Assert --
#expect(context.command == "cache.clean")
#expect(context.typeName == nil)
#expect(context.technology == nil)
#expect(context.outputJSON == nil)
#expect(context.attributes.keys.sorted() == expectedKeys)
#expect(context.metricAttributes.keys.sorted() == expectedKeys)
#expect(context.logMetadata.keys.sorted() == expectedKeys)
}

@Test("opts only output mode into technologies list telemetry")
func includesTechnologiesListOutputMode() {
// -- Arrange --
Expand Down
Loading