Skip to content
Open
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
4 changes: 2 additions & 2 deletions Sources/Core/Infrastructure/OllamaClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import Foundation

public struct OllamaClient: Client {
private let model = "llama3.2"
public let model: String
private let url = "http://localhost:11434/api/chat"
public init() {}
public init(model: String) {self.model = model}
public func send(messages: [Message]) async throws -> String {
let url = URL(string: url)!
var request = URLRequest(url: url)
Expand Down
1 change: 1 addition & 0 deletions Sources/Core/Main/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@

public protocol Client {
typealias Message = [String: String]
var model: String {get}
func send(messages: [Message]) async throws -> String
}
19 changes: 13 additions & 6 deletions Sources/Core/Main/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ public class Coordinator {
private let persistor: Persistor
private let iterator = Iterator()
public init(
reader: FileReader,
reader: FileReader = FileManager.default,
client: Client,
runner: Runner,
persistor: Persistor
persistor: Persistor = FilePersistor()
) {
self.reader = reader
self.client = client
Expand All @@ -26,16 +26,23 @@ public class Coordinator {
@discardableResult
public func generateAndSaveCode(systemPrompt: String, specsFileURL: URL, outputFileURL: URL, maxIterationCount: Int = 1) async throws -> Output {
let specs = try reader.read(specsFileURL)
let output = try await generateCode(
systemPrompt: systemPrompt,
specs: specs,
maxIterationCount: maxIterationCount
)
try persistor.persist(output.generatedCode, outputURL: outputFileURL)
return output
}

public func generateCode(systemPrompt: String, specs: String, maxIterationCount: Int = 1) async throws -> Output {
var previousOutput: Output?
let output = try await iterator.iterate(
return try await iterator.iterate(
nTimes: maxIterationCount,
until: { previousOutput = $0 ; return isSuccess($0) }
) {
try await self.generateCode(systemPrompt: systemPrompt, from: specs, previous: previousOutput)
}

try persistor.persist(output.generatedCode, outputURL: outputFileURL)
return output
}

private func generateCode(systemPrompt: String, from specs: String, previous: Output?) async throws -> Output {
Expand Down
2 changes: 1 addition & 1 deletion Sources/tddbuddy/TddBuddy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct TDDBuddy: AsyncParsableCommand {
var iterations: Int = 5

func run() async throws {
let client = OllamaClient()
let client = OllamaClient(model: "llama3.2")
let runner = LoggerDecorator(SwiftRunner())
let persistor = LoggerDecorator(FilePersistor())

Expand Down
4 changes: 2 additions & 2 deletions Tests/CoreE2ETests/IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class IntegrationTests: XCTestCase {
If your code fails to compile, the user will provide the error output for you to make adjustments.
"""
let reader = FileManager.default
let client = OllamaClient()
let client = OllamaClient(model: "llama3.2")
let runner = LoggerDecorator(SwiftRunner())
let persistor = LoggerDecorator(FilePersistor())
let sut = Coordinator(
Expand All @@ -32,7 +32,7 @@ class IntegrationTests: XCTestCase {
)
let adderSpecs = specsURL("adder.swift.txt")
let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("adder.swift.txt")
let output = try await sut.generateAndSaveCode(systemPrompt: systemPrompt, specsFileURL: adderSpecs, outputFileURL: tmpURL, maxIterationCount: 5)
let output = try await sut.generateAndSaveCode(systemPrompt: systemPrompt, specsFileURL: adderSpecs, outputFileURL: tmpURL, maxIterationCount: 2)

XCTAssertFalse(output.generatedCode.isEmpty)
XCTAssertEqual(output.procesOutput.exitCode, 0)
Expand Down
2 changes: 1 addition & 1 deletion Tests/CoreE2ETests/OllamaClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Core
class OllamaClientTests: XCTestCase {

func test_send_withRunningOllamaServer_returnsContent() async throws {
let sut = OllamaClient()
let sut = OllamaClient(model: "llama3.2")
let response = try await sut.send(messages: [["role": "user", "content": "hello"]])
XCTAssert(!response.isEmpty)
}
Expand Down
196 changes: 196 additions & 0 deletions Tests/CoreTests/DataGathering.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// © 2025 Cristian Felipe Patiño Rojas. Created on 28/5/25.

import XCTest
import Core

class DataGatheringTests: XCTestCase {
final class IterationRecorder {
private var date: Date?
private var code: String?
private let currentDate: () -> Date
var iterations: [DataGatheringTests.Iteration] = []

init(currentDate: @escaping () -> Date) {
self.currentDate = currentDate
}

func recordGeneratedCode(_ code: String) {
self.code = code
self.date = currentDate()
}

func recordOutput(_ output: Runner.ProcessOutput) {
guard let date = date, let code = code else {
print("⚠️ Skipped iteration: incomplete data")
return
}
iterations.append(
.init(
startDate: date,
generatedCode: code,
exitCode: output.exitCode,
stdErr: output.stderr,
stdOut: output.stdout
)
)
self.code = nil
self.date = nil
}
}

struct GatheredData: Equatable {
let testId: String
let modelName: String
let executions: [Execution]
}

struct Execution: Equatable {
let startDate: Date
let iterations: [Iteration]
}

struct Iteration: Equatable {
let startDate: Date
let generatedCode: String
let exitCode: Int
let stdErr: String?
let stdOut: String?
}

struct Specs {
let id: String
let content: String
}

class DataGatherer {
let client: Client
let runner: Runner
let currentDate: () -> Date

init(client: Client, runner: Runner, currentDate: @escaping () -> Date = Date.init) {
self.client = client
self.runner = runner
self.currentDate = currentDate
}

func gatherData(systemPrompt: String, specs: Specs, executionCount: Int, iterationCount: Int) async throws -> GatheredData {

var executions = [Execution]()

let currentDate = currentDate
for _ in (1...executionCount) {
let initialTimestamp = currentDate()
let recorder = IterationRecorder(currentDate: currentDate)
let clientSpy = ClientSpy(client: client, onResult: recorder.recordGeneratedCode)
let runnerSpy = RunnerSpy(runner: runner, onResult: recorder.recordOutput)
let coordinator = Coordinator(client: clientSpy, runner: runnerSpy)
let _ = try await coordinator.generateCode(
systemPrompt: systemPrompt,
specs: specs.content,
maxIterationCount: iterationCount
)
executions.append(
Execution(
startDate: initialTimestamp,
iterations: recorder.iterations
)
)
}

return GatheredData(testId: specs.id, modelName: client.model, executions: executions)
}

class ClientSpy: Client {
let client: Client

init(client: Client, onResult: @escaping (String) -> Void) {
self.client = client
self.onResponse = onResult
}
var model: String {client.model}
let onResponse: (String) -> Void
func send(messages: [Message]) async throws -> String {
let response = try await client.send(messages: messages)
onResponse(response)
return response
}
}

class RunnerSpy: Runner {
let runner: Runner

init(runner: Runner, onResult: @escaping (ProcessOutput) -> Void) {
self.runner = runner
self.onProcessOutput = onResult
}
let onProcessOutput: (ProcessOutput) -> Void
func run(_ code: String) throws -> ProcessOutput {
let output = try runner.run(code)
onProcessOutput(output)
return output
}
}

}

func test_gatherData_recordsExpectedData() async throws {
struct ClientStub: Client {
let model: String
let result: String
func send(messages: [Message]) async throws -> String {
result
}
}

struct RunnerStub: Runner {
let result: ProcessOutput
func run(_ code: String) throws -> ProcessOutput {
return result
}
}

let executionCount = 5
let iterationCount = 5
let timestamp = Date()
let client = ClientStub(model: "fake model", result: "any generated code")
let runner = RunnerStub(result: (stdout: "", stderr: "any stderr", exitCode: 1))
let sut = DataGatherer(client: client, runner: runner, currentDate: { timestamp })
let specs = Specs(
id: "adder_spec",
content: """
func test_adder() {
let sut = Adder(1,3)
assert(sut.result == 4)
}
test_adder()
"""
)

let data = try await sut.gatherData(
systemPrompt: "any system prompt",
specs: specs,
executionCount: executionCount,
iterationCount: iterationCount
)

let iteration = Iteration(
startDate: timestamp,
generatedCode: "any generated code",
exitCode: 1,
stdErr: "any stderr",
stdOut: ""
)

let iterations = Array(repeating: iteration, count: iterationCount)
let execution = Execution(startDate: timestamp, iterations: iterations)
let executions = Array(repeating: execution, count: executionCount)

let expectedData = GatheredData(
testId: "adder_spec",
modelName: "fake model",
executions: executions
)

XCTAssertEqual(data, expectedData)
}
}
3 changes: 3 additions & 0 deletions Tests/CoreTests/UseCases/Helpers/CoordinatorTests+Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ extension CoordinatorTests {

struct ClientStub: Client {
let result: Result<String, Error>
var model: String { "any model" }
func send(messages: [Message]) async throws -> String {
try result.get()
}
Expand All @@ -57,6 +58,7 @@ extension CoordinatorTests {
}

struct ClientDummy: Client {
var model: String {"any model"}
func send(messages: [Message]) async throws -> String {
""
}
Expand All @@ -79,6 +81,7 @@ extension CoordinatorTests {
extension CoordinatorTests {
class ClientSpy: Client {
var messages = [[Message]]()
var model: String {"any model"}
func send(messages: [Message]) async throws -> String {
self.messages.append(messages)
return "any generated code"
Expand Down
1 change: 1 addition & 0 deletions todo.taskpaper
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
- Test CLI through `TDDBuddy.main() ` testing.
- Allow persisting attempts with logs.
- Add more LLM Clients and allow customization from command line.
- Add memory leak tracking