Skip to content

ISSUE-3: Use Database to store reports #6

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
add tests for DuckDB-integration
  • Loading branch information
elmoritz committed Nov 19, 2024
commit 4c3b0698c1898ac3a7b59e052eedef8f37460884
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension PackageDescription.Target {
typealias MyPackage = PackageDescription.Target

let package = Package(
name: "CodeCoverage",
name: "DerivedDataTool",
platforms: [
.macOS(.v14),
],
Expand Down Expand Up @@ -168,6 +168,7 @@ let package = Package(
path: "Tests/HelperTests",
resources: [
.process("Resources/TestData.json"),
.process("Resources/TestData-tiny.json"),
]
),

Expand Down
154 changes: 154 additions & 0 deletions Sources/Helper/DuckDB/CoverageReportStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// File.swift
// CodeCoverage
//
// Created by Moritz Ellerbrock on 18.11.24.
//

import Foundation
import DuckDB
import Shared
import TabularData

public struct DBKey {
let value: String
let application: String
init (date: Foundation.Date, application: String) {
value = DateFormat.yearMontDay.string(from: date)
self.application = application
}
}

public protocol CoverageReportStore {
func getAllEntries(for application: String) async throws -> [CoverageReport]
func getEntry(for key: DBKey) async throws -> CoverageReport?
func addEntry(_ entry: CoverageReport, for key: DBKey) async throws
func updateEntry(_ entry: CoverageReport, for key: DBKey) async throws
func removeEntry(for key: DBKey) async throws

static func makeKey(from date: Foundation.Date, application: String) -> DBKey
static func makeStore(dbType: DuckDBConnection.DBTType) async throws -> CoverageReportStore
}

extension CoverageReportStore {
public static func makeKey(from date: Foundation.Date, application: String) -> DBKey {
DBKey(date: date, application: application)
}

public static func makeStore(dbType: DuckDBConnection.DBTType) async throws -> CoverageReportStore {
let dbConnector = try DuckDBConnection(with: dbType)
let store = try CoverageReportStoreImpl(duckDBConnection: dbConnector)

try await store.setup()

return store
}
}

public final class CoverageReportStoreImpl {

let duckDBConnection: DuckDBConnection

init(duckDBConnection: DuckDBConnection) throws {
self.duckDBConnection = duckDBConnection
}

func connection() async -> Connection {
duckDBConnection.connection
}

func setup() async throws {
do {
_ = try duckDBConnection.connection.query(
"""
CREATE TABLE coverage_reports (
application VARCHAR(255) NOT NULL,
date VARCHAR(15) NOT NULL UNIQUE,
coverage JSON NOT NULL,
PRIMARY KEY (date)
);
"""
)
} catch {
print(error)
throw error
}
}
}

extension CoverageReportStoreImpl: CoverageReportStore {
public func getAllEntries(for application: String) async throws -> [Shared.CoverageReport] {
let result = try await connection().query("""
SELECT * FROM coverage_reports
GROUP BY date
ORDER BY date;
""")

print(result)

return []
}

public func getEntry(for key: DBKey) async throws -> Shared.CoverageReport? {
let result = try await connection().query("""
SELECT coverage
FROM coverage_reports
WHERE date = \(key.value);
""")

print(result)
return nil
}

public func addEntry(_ entry: Shared.CoverageReport, for key: DBKey) async throws {
do {
let result = try await connection().query(
"INSERT INTO coverage_reports (application, date, coverage) VALUES (\(key.application), \(key.value), \(encodeToJSONString(entry));")

print(result)
print("DONE")
} catch {
print(error)
print(encodeToJSONString(entry))
throw error
}
}

public func updateEntry(_ entry: Shared.CoverageReport, for key: DBKey) async throws {
let result = try await connection().query("""
INSERT INTO coverage_reports (application, date, coverage)
VALUES (\(key.application), \(key.value), \(entry)
ON DUPLICATE KEY UPDATE
coverage = VALUES(\(entry));
""")

print(result)
print("DONE")
}

public func removeEntry(for key: DBKey) async throws {
let result = try await connection().query("""
DELETE FROM coverage_reports
WHERE date = \(key.value);

""")

print(result)
print("DONE")
}

private func encodeToJSONString(_ value: Codable) -> String {
do {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(value)
let jsonObject = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: .sortedKeys)
return String(decoding: jsonData, as: UTF8.self)
} catch {
return "Encoding error: \(error.localizedDescription)"
}
}


}
44 changes: 44 additions & 0 deletions Sources/Helper/DuckDB/DuckDBConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// File.swift
// CodeCoverage
//
// Created by Moritz Ellerbrock on 07.11.24.
//

import Foundation
import DuckDB

public enum DuckDBConnectionError: Error {
case initializationFailed(String)
}

public actor DuckDBConnection {
public enum DBTType {
case inMemory
case local(url: URL)
}

private let type: DBTType
let database: Database
let connection: Connection


public init(with dbType: DBTType) throws {
self.type = dbType
self.database = try Self.makeDatabase(type: self.type)
self.connection = try database.connect()
}

private static func makeDatabase(type: DBTType) throws -> Database {
do {
switch type {
case .inMemory:
return try Database.init(store: .inMemory)
case let .local(url):
return try Database.init(store: .file(at: url))
}
} catch {
throw DuckDBConnectionError.initializationFailed("Database creation failed with: \(error)")
}
}
}
8 changes: 0 additions & 8 deletions Sources/Helper/DuckDB/File.swift

This file was deleted.

21 changes: 21 additions & 0 deletions Sources/Shared/Coverage/CoverageReport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ public extension Coverage {
}
}

// MARK: wrapping object

public struct DuckDBCoverage: Codable, Hashable {
public let application: String
public let date: String
public let coverage: CoverageReport

public init(application: String, date: Date, coverage: CoverageReport) {
self.application = application
self.date = DateFormat.yearMontDay.string(from: date)
self.coverage = coverage
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.application = try container.decode(String.self, forKey: .application)
self.date = try container.decode(String.self, forKey: .date)
self.coverage = try container.decode(CoverageReport.self, forKey: .coverage)
}
}

// MARK: - CoverageReport

public struct CoverageReport: Coverage {
Expand Down
15 changes: 15 additions & 0 deletions Sources/Shared/Dateformater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,31 @@

import Foundation


public enum DateFormat: CaseIterable {
/// yyyy-MM-dd
case yearMontDay
/// yyyy-MM-dd HH:mm:ss
case YearMonthDayHoursMinutesSeconds
/// yyyy.MM.dd_HH-mm-ss-ZZZZ
case YearMonthDayHoursMinutesSecondsAndTimeZone
/// dd/MM/yyyy
case dayMonthYear
/// dd MMM yyyy
case dayAbbreviatedMonthYear
/// MMMM dd, yyyy
case fullMonthDayYear
/// EEEE, MMMM dd, yyyy
case fullWeekdayFullMonthNameDayYear
/// h:mm a
case hoursMinutesWithAmPmIndicator
/// HH:mm:ss
case hoursMinutesSecondsIn24hFormat
/// yyyy-MM-dd'T'HH:mm:ss.SSSZ
case iso8601Format
/// MMM dd, yyyy 'at' h:mm a
case abbreviatedMonthDayYearTimeInAmPmFormat
/// MMM dd, yyyy 'at' h:mm:ss
case abbreviatedMonthDayYearTimeIn24hFormat

private var format: String {
Expand Down Expand Up @@ -52,6 +65,8 @@ public enum DateFormat: CaseIterable {

private var dateFormatter: DateFormatter {
let dateformatter = DateFormatter()
dateformatter.calendar = Calendar(identifier: .gregorian)
dateformatter.timeZone = TimeZone(secondsFromGMT: 0)
dateformatter.dateFormat = format
return dateformatter
}
Expand Down
55 changes: 55 additions & 0 deletions Tests/HelperTests/DuckDbIntergationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// DuckDbIntergationTests.swift
// CodeCoverage
//
// Created by Moritz Ellerbrock on 18.11.24.
//

import XCTest
import Foundation
import Shared
@testable import Helper

final class DuckDbIntergationTests: XCTestCase {
enum DummyError: Error {
case dummy
}


func testAddEntry() async throws {
let store = try Self.makeCoverageStore()
let testData = try Self.makeTestDate()
let dbKey = CoverageReportStoreImpl.makeKey(from: testData.fileInfo.date, application: testData.fileInfo.application)
try await store.addEntry(testData.coverage, for: dbKey)
}
}


extension DuckDbIntergationTests {
static func jsonDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}

static func makeCoverageStore() throws -> CoverageReportStore {
let dbConnector = try DuckDBConnection(with: .inMemory)
return try CoverageReportStoreImpl(duckDBConnection: dbConnector)
}

static func makeTestDate() throws -> CoverageMetaReport {
guard let url = Bundle.module.url(forResource: "TestData-tiny", withExtension: "json") else {
XCTFail("Resource is missing")
throw DummyError.dummy
}

let urlData = try Data(contentsOf: url)

guard let savedReport = try? Self.jsonDecoder().decode(CoverageMetaReport.self, from: urlData) else {
XCTFail("Resource Content is missing")
throw DummyError.dummy
}

return savedReport
}
}
1 change: 1 addition & 0 deletions Tests/HelperTests/Resources/TestData-tiny.json

Large diffs are not rendered by default.

Loading