Add support for sampling (#119)

* Add UnitInterval type

* Add test coverage for UnitInterval

* Add support for sampling

* Add integration tests for sampling
This commit is contained in:
Mattt 2025-05-26 04:57:24 -07:00 • committed by GitHub
commit fac4df6b1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1584 additions and 0 deletions

View file

@ -0,0 +1,806 @@
import Logging
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
@testable import MCP
#if canImport(System)
import System
#else
@preconcurrency import SystemPackage
#endif
@Suite("Sampling Tests")
struct SamplingTests {
@Test("Sampling.Message encoding and decoding")
func testSamplingMessageCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text content
let textMessage = Sampling.Message(
role: .user,
content: .text("Hello, world!")
)
let textData = try encoder.encode(textMessage)
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
#expect(decodedTextMessage.role == .user)
if case .text(let text) = decodedTextMessage.content {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
// Test image content
let imageMessage = Sampling.Message(
role: .assistant,
content: .image(data: "base64imagedata", mimeType: "image/png")
)
let imageData = try encoder.encode(imageMessage)
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
#expect(decodedImageMessage.role == .assistant)
if case .image(let data, let mimeType) = decodedImageMessage.content {
#expect(data == "base64imagedata")
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
}
@Test("ModelPreferences encoding and decoding")
func testModelPreferencesCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let preferences = Sampling.ModelPreferences(
hints: [
Sampling.ModelPreferences.Hint(name: "claude-4"),
Sampling.ModelPreferences.Hint(name: "gpt-4.1"),
],
costPriority: 0.8,
speedPriority: 0.3,
intelligencePriority: 0.9
)
let data = try encoder.encode(preferences)
let decoded = try decoder.decode(Sampling.ModelPreferences.self, from: data)
#expect(decoded.hints?.count == 2)
#expect(decoded.hints?[0].name == "claude-4")
#expect(decoded.hints?[1].name == "gpt-4.1")
#expect(decoded.costPriority?.doubleValue == 0.8)
#expect(decoded.speedPriority?.doubleValue == 0.3)
#expect(decoded.intelligencePriority?.doubleValue == 0.9)
}
@Test("ContextInclusion encoding and decoding")
func testContextInclusionCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let contexts: [Sampling.ContextInclusion] = [.none, .thisServer, .allServers]
for context in contexts {
let data = try encoder.encode(context)
let decoded = try decoder.decode(Sampling.ContextInclusion.self, from: data)
#expect(decoded == context)
}
}
@Test("StopReason encoding and decoding")
func testStopReasonCoding() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let reasons: [Sampling.StopReason] = [.endTurn, .stopSequence, .maxTokens]
for reason in reasons {
let data = try encoder.encode(reason)
let decoded = try decoder.decode(Sampling.StopReason.self, from: data)
#expect(decoded == reason)
}
}
@Test("CreateMessage request parameters")
func testCreateMessageParameters() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let messages = [
Sampling.Message(role: .user, content: .text("What is the weather like?")),
Sampling.Message(
role: .assistant, content: .text("I need to check the weather for you.")),
]
let modelPreferences = Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "claude-4-sonnet")],
costPriority: 0.5,
speedPriority: 0.7,
intelligencePriority: 0.9
)
let parameters = CreateSamplingMessage.Parameters(
messages: messages,
modelPreferences: modelPreferences,
systemPrompt: "You are a helpful weather assistant.",
includeContext: .thisServer,
temperature: 0.7,
maxTokens: 150,
stopSequences: ["END", "STOP"],
metadata: ["provider": "test"]
)
let data = try encoder.encode(parameters)
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
#expect(decoded.messages.count == 2)
#expect(decoded.messages[0].role == .user)
#expect(decoded.systemPrompt == "You are a helpful weather assistant.")
#expect(decoded.includeContext == .thisServer)
#expect(decoded.temperature == 0.7)
#expect(decoded.maxTokens == 150)
#expect(decoded.stopSequences?.count == 2)
#expect(decoded.stopSequences?[0] == "END")
#expect(decoded.stopSequences?[1] == "STOP")
#expect(decoded.metadata?["provider"]?.stringValue == "test")
}
@Test("CreateMessage result")
func testCreateMessageResult() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let result = CreateSamplingMessage.Result(
model: "claude-4-sonnet",
stopReason: .endTurn,
role: .assistant,
content: .text("The weather is sunny and 75°F.")
)
let data = try encoder.encode(result)
let decoded = try decoder.decode(CreateSamplingMessage.Result.self, from: data)
#expect(decoded.model == "claude-4-sonnet")
#expect(decoded.stopReason == .endTurn)
#expect(decoded.role == .assistant)
if case .text(let text) = decoded.content {
#expect(text == "The weather is sunny and 75°F.")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("CreateMessage request creation")
func testCreateMessageRequest() throws {
let messages = [
Sampling.Message(role: .user, content: .text("Hello"))
]
let request = CreateSamplingMessage.request(
.init(
messages: messages,
maxTokens: 100
)
)
#expect(request.method == "sampling/createMessage")
#expect(request.params.messages.count == 1)
#expect(request.params.maxTokens == 100)
}
@Test("Server capabilities include sampling")
func testServerCapabilitiesIncludeSampling() throws {
let capabilities = Server.Capabilities(
sampling: .init()
)
#expect(capabilities.sampling != nil)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Server.Capabilities.self, from: data)
#expect(decoded.sampling != nil)
}
@Test("Client capabilities include sampling")
func testClientCapabilitiesIncludeSampling() throws {
let capabilities = Client.Capabilities(
sampling: .init()
)
#expect(capabilities.sampling != nil)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Client.Capabilities.self, from: data)
#expect(decoded.sampling != nil)
}
@Test("Client sampling handler registration")
func testClientSamplingHandlerRegistration() async throws {
let client = Client(name: "TestClient", version: "1.0")
// Test that sampling handler can be registered
let handlerClient = await client.withSamplingHandler { parameters in
// Mock handler that returns a simple response
return CreateSamplingMessage.Result(
model: "test-model",
stopReason: .endTurn,
role: .assistant,
content: .text("Test response")
)
}
// Should return self for method chaining
#expect(handlerClient === client)
}
@Test("Server sampling request method")
func testServerSamplingRequestMethod() async throws {
let transport = MockTransport()
let server = Server(
name: "TestServer",
version: "1.0",
capabilities: .init(sampling: .init())
)
try await server.start(transport: transport)
// Test that server can attempt to request sampling
let messages = [
Sampling.Message(role: .user, content: .text("Test message"))
]
do {
_ = try await server.requestSampling(
messages: messages,
maxTokens: 100
)
#expect(
Bool(false),
"Should have thrown an error for unimplemented bidirectional communication")
} catch let error as MCPError {
if case .internalError(let message) = error {
#expect(
message?.contains("Bidirectional sampling requests not yet implemented") == true
)
} else {
#expect(Bool(false), "Expected internalError, got \(error)")
}
} catch {
#expect(Bool(false), "Expected MCPError, got \(error)")
}
await server.stop()
}
@Test("Sampling message content JSON format")
func testSamplingMessageContentJSONFormat() throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
// Test text content JSON format
let textContent = Sampling.Message.Content.text("Hello")
let textData = try encoder.encode(textContent)
let textJSON = String(data: textData, encoding: .utf8)!
#expect(textJSON.contains("\"type\":\"text\""))
#expect(textJSON.contains("\"text\":\"Hello\""))
// Test image content JSON format
let imageContent = Sampling.Message.Content.image(data: "base64data", mimeType: "image/png")
let imageData = try encoder.encode(imageContent)
let imageJSON = String(data: imageData, encoding: .utf8)!
#expect(imageJSON.contains("\"type\":\"image\""))
#expect(imageJSON.contains("\"data\":\"base64data\""))
#expect(imageJSON.contains("\"mimeType\":\"image\\/png\""))
}
@Test("UnitInterval in Sampling.ModelPreferences")
func testUnitIntervalInModelPreferences() throws {
// Test that UnitInterval validation works in Sampling.ModelPreferences
let validPreferences = Sampling.ModelPreferences(
costPriority: 0.5,
speedPriority: 1.0,
intelligencePriority: 0.0
)
#expect(validPreferences.costPriority?.doubleValue == 0.5)
#expect(validPreferences.speedPriority?.doubleValue == 1.0)
#expect(validPreferences.intelligencePriority?.doubleValue == 0.0)
// Test JSON encoding/decoding preserves UnitInterval constraints
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(validPreferences)
let decoded = try decoder.decode(Sampling.ModelPreferences.self, from: data)
#expect(decoded.costPriority?.doubleValue == 0.5)
#expect(decoded.speedPriority?.doubleValue == 1.0)
#expect(decoded.intelligencePriority?.doubleValue == 0.0)
}
}
@Suite("Sampling Integration Tests")
struct SamplingIntegrationTests {
@Test(
.timeLimit(.minutes(1))
)
func testSamplingCapabilitiesNegotiation() async throws {
let (clientToServerRead, clientToServerWrite) = try FileDescriptor.pipe()
let (serverToClientRead, serverToClientWrite) = try FileDescriptor.pipe()
var logger = Logger(
label: "mcp.test.sampling",
factory: { StreamLogHandler.standardError(label: $0) })
logger.logLevel = .debug
let serverTransport = StdioTransport(
input: clientToServerRead,
output: serverToClientWrite,
logger: logger
)
let clientTransport = StdioTransport(
input: serverToClientRead,
output: clientToServerWrite,
logger: logger
)
// Server with sampling capability
let server = Server(
name: "SamplingTestServer",
version: "1.0.0",
capabilities: .init(
sampling: .init(), // Enable sampling
tools: .init()
)
)
// Client (capabilities will be set during initialization)
let client = Client(
name: "SamplingTestClient",
version: "1.0"
)
try await server.start(transport: serverTransport)
try await client.connect(transport: clientTransport)
// Test initialization and capability negotiation
let initTask = Task {
let result = try await client.initialize()
#expect(result.serverInfo.name == "SamplingTestServer")
#expect(result.serverInfo.version == "1.0.0")
#expect(
result.capabilities.sampling != nil, "Server should advertise sampling capability")
#expect(result.protocolVersion == Version.latest)
}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await Task.sleep(for: .seconds(1))
initTask.cancel()
throw CancellationError()
}
group.addTask {
try await initTask.value
}
try await group.next()
group.cancelAll()
}
await server.stop()
await client.disconnect()
try? clientToServerRead.close()
try? clientToServerWrite.close()
try? serverToClientRead.close()
try? serverToClientWrite.close()
}
@Test(
.timeLimit(.minutes(1))
)
func testSamplingHandlerRegistration() async throws {
let client = Client(
name: "SamplingHandlerTestClient",
version: "1.0"
)
// Register sampling handler
let handlerClient = await client.withSamplingHandler { parameters in
// Mock LLM response
return CreateSamplingMessage.Result(
model: "test-model-v1",
stopReason: .endTurn,
role: .assistant,
content: .text("This is a test completion from the mock LLM.")
)
}
// Verify method chaining works
#expect(
handlerClient === client, "withSamplingHandler should return self for method chaining")
// Note: We can't test the actual handler invocation without bidirectional transport,
// but we can verify the handler registration doesn't crash and returns correctly
}
@Test(
.timeLimit(.minutes(1))
)
func testServerSamplingRequestAPI() async throws {
let transport = MockTransport()
let server = Server(
name: "SamplingRequestTestServer",
version: "1.0",
capabilities: .init(sampling: .init())
)
try await server.start(transport: transport)
// Test sampling request with comprehensive parameters
let messages = [
Sampling.Message(
role: .user,
content: .text("Analyze the following data and provide insights:")
),
Sampling.Message(
role: .user,
content: .text("Sales data: Q1: $100k, Q2: $150k, Q3: $200k, Q4: $180k")
),
]
let modelPreferences = Sampling.ModelPreferences(
hints: [
Sampling.ModelPreferences.Hint(name: "claude-4-sonnet"),
Sampling.ModelPreferences.Hint(name: "gpt-4.1"),
],
costPriority: 0.3,
speedPriority: 0.7,
intelligencePriority: 0.9
)
// Test that the API accepts all parameters correctly
do {
_ = try await server.requestSampling(
messages: messages,
modelPreferences: modelPreferences,
systemPrompt: "You are a business analyst expert.",
includeContext: .thisServer,
temperature: 0.7,
maxTokens: 500,
stopSequences: ["END_ANALYSIS", "\n\n---"],
metadata: [
"requestId": "test-123",
"priority": "high",
"department": "analytics",
]
)
#expect(Bool(false), "Should throw error for unimplemented bidirectional communication")
} catch let error as MCPError {
if case .internalError(let message) = error {
#expect(
message?.contains("Bidirectional sampling requests not yet implemented")
== true,
"Should indicate bidirectional communication not implemented"
)
} else {
#expect(Bool(false), "Expected internalError, got \(error)")
}
} catch {
#expect(Bool(false), "Expected MCPError, got \(error)")
}
await server.stop()
}
@Test(
.timeLimit(.minutes(1))
)
func testSamplingMessageTypes() async throws {
// Test comprehensive message content types
let textMessage = Sampling.Message(
role: .user,
content: .text("What do you see in this data?")
)
let imageMessage = Sampling.Message(
role: .user,
content: .image(
data:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
mimeType: "image/png"
)
)
// Test encoding/decoding of different message types
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text message
let textData = try encoder.encode(textMessage)
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
#expect(decodedTextMessage.role == .user)
if case .text(let text) = decodedTextMessage.content {
#expect(text == "What do you see in this data?")
} else {
#expect(Bool(false), "Expected text content")
}
// Test image message
let imageData = try encoder.encode(imageMessage)
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
#expect(decodedImageMessage.role == .user)
if case .image(let data, let mimeType) = decodedImageMessage.content {
#expect(data.contains("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ"))
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
}
@Test(
.timeLimit(.minutes(1))
)
func testSamplingResultTypes() async throws {
// Test different result content types and stop reasons
let textResult = CreateSamplingMessage.Result(
model: "claude-4-sonnet",
stopReason: .endTurn,
role: .assistant,
content: .text(
"Based on the sales data analysis, I can see a strong upward trend through Q3, with a slight decline in Q4. This suggests seasonal factors or market saturation."
)
)
let imageResult = CreateSamplingMessage.Result(
model: "dall-e-3",
stopReason: .maxTokens,
role: .assistant,
content: .image(
data: "generated_chart_base64_data_here",
mimeType: "image/png"
)
)
let stopSequenceResult = CreateSamplingMessage.Result(
model: "gpt-4.1",
stopReason: .stopSequence,
role: .assistant,
content: .text("Analysis complete.\nEND_ANALYSIS")
)
// Test encoding/decoding of different result types
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text result
let textData = try encoder.encode(textResult)
let decodedTextResult = try decoder.decode(
CreateSamplingMessage.Result.self, from: textData)
#expect(decodedTextResult.model == "claude-4-sonnet")
#expect(decodedTextResult.stopReason == .endTurn)
#expect(decodedTextResult.role == .assistant)
// Test image result
let imageData = try encoder.encode(imageResult)
let decodedImageResult = try decoder.decode(
CreateSamplingMessage.Result.self, from: imageData)
#expect(decodedImageResult.model == "dall-e-3")
#expect(decodedImageResult.stopReason == .maxTokens)
// Test stop sequence result
let stopData = try encoder.encode(stopSequenceResult)
let decodedStopResult = try decoder.decode(
CreateSamplingMessage.Result.self, from: stopData)
#expect(decodedStopResult.stopReason == .stopSequence)
}
@Test(
.timeLimit(.minutes(1))
)
func testSamplingErrorHandling() async throws {
let transport = MockTransport()
let server = Server(
name: "ErrorTestServer",
version: "1.0",
capabilities: .init() // No sampling capability
)
try await server.start(transport: transport)
// Test sampling request on server without sampling capability
let messages = [
Sampling.Message(role: .user, content: .text("Test message"))
]
do {
_ = try await server.requestSampling(
messages: messages,
maxTokens: 100
)
#expect(Bool(false), "Should throw error for missing connection")
} catch let error as MCPError {
if case .internalError(let message) = error {
#expect(
message?.contains("Server connection not initialized") == true
|| message?.contains("Bidirectional sampling requests not yet implemented")
== true,
"Should indicate connection or implementation issue"
)
} else {
#expect(Bool(false), "Expected internalError, got \(error)")
}
} catch {
#expect(Bool(false), "Expected MCPError, got \(error)")
}
await server.stop()
}
@Test(
.timeLimit(.minutes(1))
)
func testSamplingParameterValidation() async throws {
// Test parameter validation and edge cases
let validMessages = [
Sampling.Message(role: .user, content: .text("Valid message"))
]
_ = [Sampling.Message]() // Test empty messages array
// Test with valid parameters
let validParams = CreateSamplingMessage.Parameters(
messages: validMessages,
maxTokens: 100
)
#expect(validParams.messages.count == 1)
#expect(validParams.maxTokens == 100)
// Test with comprehensive parameters
let comprehensiveParams = CreateSamplingMessage.Parameters(
messages: validMessages,
modelPreferences: Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "claude-4")],
costPriority: 0.5,
speedPriority: 0.8,
intelligencePriority: 0.9
),
systemPrompt: "You are a helpful assistant.",
includeContext: .allServers,
temperature: 0.7,
maxTokens: 500,
stopSequences: ["STOP", "END"],
metadata: [
"sessionId": "test-session-123",
"userId": "user-456",
]
)
#expect(comprehensiveParams.messages.count == 1)
#expect(comprehensiveParams.modelPreferences?.hints?.count == 1)
#expect(comprehensiveParams.systemPrompt == "You are a helpful assistant.")
#expect(comprehensiveParams.includeContext == .allServers)
#expect(comprehensiveParams.temperature == 0.7)
#expect(comprehensiveParams.maxTokens == 500)
#expect(comprehensiveParams.stopSequences?.count == 2)
#expect(comprehensiveParams.metadata?.count == 2)
// Test encoding/decoding of comprehensive parameters
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(comprehensiveParams)
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
#expect(decoded.messages.count == 1)
#expect(decoded.modelPreferences?.costPriority?.doubleValue == 0.5)
#expect(decoded.systemPrompt == "You are a helpful assistant.")
#expect(decoded.includeContext == .allServers)
#expect(decoded.temperature == 0.7)
#expect(decoded.maxTokens == 500)
#expect(decoded.stopSequences?[0] == "STOP")
#expect(decoded.metadata?["sessionId"]?.stringValue == "test-session-123")
}
@Test(
.timeLimit(.minutes(1))
)
func testSamplingWorkflowScenarios() async throws {
// Test realistic sampling workflow scenarios
// Scenario 1: Data Analysis Request
let dataAnalysisMessages = [
Sampling.Message(
role: .user,
content: .text("Please analyze the following customer feedback data:")
),
Sampling.Message(
role: .user,
content: .text(
"""
Feedback Summary:
- 85% positive sentiment
- Top complaints: shipping delays (12%), product quality (8%)
- Top praise: customer service (45%), product features (40%)
- NPS Score: 72
""")
),
]
let dataAnalysisParams = CreateSamplingMessage.Parameters(
messages: dataAnalysisMessages,
modelPreferences: Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "claude-4-sonnet")],
speedPriority: 0.3,
intelligencePriority: 0.9
),
systemPrompt: "You are an expert business analyst. Provide actionable insights.",
includeContext: .thisServer,
temperature: 0.3, // Lower temperature for analytical tasks
maxTokens: 400,
stopSequences: ["---END---"],
metadata: ["analysisType": "customer-feedback"]
)
// Scenario 2: Creative Content Generation
let creativeMessages = [
Sampling.Message(
role: .user,
content: .text(
"Write a compelling product description for a new smart home device.")
)
]
let creativeParams = CreateSamplingMessage.Parameters(
messages: creativeMessages,
modelPreferences: Sampling.ModelPreferences(
hints: [Sampling.ModelPreferences.Hint(name: "gpt-4.1")],
costPriority: 0.4,
speedPriority: 0.6,
intelligencePriority: 0.8
),
systemPrompt: "You are a creative marketing copywriter.",
temperature: 0.8, // Higher temperature for creativity
maxTokens: 200,
metadata: ["contentType": "marketing-copy"]
)
// Test parameter encoding for both scenarios
let encoder = JSONEncoder()
let analysisData = try encoder.encode(dataAnalysisParams)
let creativeData = try encoder.encode(creativeParams)
// Verify both encode successfully
#expect(analysisData.count > 0)
#expect(creativeData.count > 0)
// Test decoding
let decoder = JSONDecoder()
let decodedAnalysis = try decoder.decode(
CreateSamplingMessage.Parameters.self, from: analysisData)
let decodedCreative = try decoder.decode(
CreateSamplingMessage.Parameters.self, from: creativeData)
#expect(decodedAnalysis.temperature == 0.3)
#expect(decodedCreative.temperature == 0.8)
#expect(decodedAnalysis.modelPreferences?.intelligencePriority?.doubleValue == 0.9)
#expect(decodedCreative.modelPreferences?.costPriority?.doubleValue == 0.4)
}
}

View file

@ -0,0 +1,219 @@
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
@testable import MCP
@Suite("UnitInterval Tests")
struct UnitIntervalTests {
@Test("Valid literal initialization")
func testValidLiteralInitialization() throws {
let zero: UnitInterval = 0.0
#expect(zero.doubleValue == 0.0)
let half: UnitInterval = 0.5
#expect(half.doubleValue == 0.5)
let one: UnitInterval = 1.0
#expect(one.doubleValue == 1.0)
let quarter: UnitInterval = 0.25
#expect(quarter.doubleValue == 0.25)
}
@Test("Valid failable initialization with runtime values")
func testValidFailableInitialization() throws {
// Test with runtime computed values to force use of failable initializer
let values = [0.0, 0.5, 1.0, 0.25]
for value in values {
let computed = value * 1.0 // Force runtime computation
let interval = UnitInterval(computed)
#expect(interval != nil)
#expect(interval!.doubleValue == value)
}
}
@Test("Invalid failable initialization")
func testInvalidFailableInitialization() throws {
// Test with runtime computed values to force use of failable initializer
let invalidValues = [-0.1, 1.1, 100.0, -100.0]
for value in invalidValues {
let computed = value * 1.0 // Force runtime computation
let interval = UnitInterval(computed)
#expect(interval == nil)
}
}
@Test("Boundary and edge case values")
func testBoundaryAndEdgeCaseValues() throws {
// Test exact boundary values
let exactZero = 0.0 * 1.0
let zero = UnitInterval(exactZero)
#expect(zero != nil)
let exactOne = 1.0 * 1.0
let one = UnitInterval(exactOne)
#expect(one != nil)
// Test machine precision boundaries
let justAboveZero = Double.ulpOfOne * 1.0
let aboveZero = UnitInterval(justAboveZero)
#expect(aboveZero != nil)
let justBelowOne = (1.0 - Double.ulpOfOne) * 1.0
let belowOne = UnitInterval(justBelowOne)
#expect(belowOne != nil)
// Test very small positive value
let tinyValue = 1e-10 * 1.0
let tiny = UnitInterval(tinyValue)
#expect(tiny != nil)
#expect(tiny!.doubleValue == 1e-10)
// Test value very close to 1
let almostOneValue = 0.9999999999 * 1.0
let almostOne = UnitInterval(almostOneValue)
#expect(almostOne != nil)
#expect(almostOne!.doubleValue == 0.9999999999)
}
@Test("Float literal initialization")
func testFloatLiteralInitialization() throws {
let zero: UnitInterval = 0.0
#expect(zero.doubleValue == 0.0)
let half: UnitInterval = 0.5
#expect(half.doubleValue == 0.5)
let one: UnitInterval = 1.0
#expect(one.doubleValue == 1.0)
let quarter: UnitInterval = 0.25
#expect(quarter.doubleValue == 0.25)
}
@Test("Integer literal initialization")
func testIntegerLiteralInitialization() throws {
let zero: UnitInterval = 0
#expect(zero.doubleValue == 0.0)
let one: UnitInterval = 1
#expect(one.doubleValue == 1.0)
}
@Test("Comparable conformance")
func testComparable() throws {
let zero: UnitInterval = 0.0
let quarter: UnitInterval = 0.25
let half: UnitInterval = 0.5
let one: UnitInterval = 1.0
#expect(zero < quarter)
#expect(quarter < half)
#expect(half < one)
#expect(zero < one)
#expect(!(quarter < zero))
#expect(!(half < quarter))
#expect(!(one < half))
#expect(zero <= quarter)
#expect(quarter <= half)
#expect(half <= one)
#expect(zero <= zero)
#expect(quarter > zero)
#expect(half > quarter)
#expect(one > half)
#expect(quarter >= zero)
#expect(half >= quarter)
#expect(one >= half)
#expect(one >= one)
}
@Test("Equality and hashing")
func testEqualityAndHashing() throws {
let half1: UnitInterval = 0.5
let half2: UnitInterval = 0.5
let quarter: UnitInterval = 0.25
#expect(half1 == half2)
#expect(half1 != quarter)
#expect(half1.hashValue == half2.hashValue)
}
@Test("String description")
func testStringDescription() throws {
let zero: UnitInterval = 0.0
#expect(zero.description == "0.0")
let half: UnitInterval = 0.5
#expect(half.description == "0.5")
let one: UnitInterval = 1.0
#expect(one.description == "1.0")
let quarter: UnitInterval = 0.25
#expect(quarter.description == "0.25")
}
@Test("JSON encoding and decoding")
func testJSONCodable() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let original: UnitInterval = 0.75
let data = try encoder.encode(original)
let decoded = try decoder.decode(UnitInterval.self, from: data)
#expect(decoded == original)
#expect(decoded.doubleValue == 0.75)
}
@Test("JSON decoding with invalid values")
func testJSONDecodingInvalidValues() throws {
let decoder = JSONDecoder()
// Test negative value
let negativeJSON = "-0.5".data(using: .utf8)!
#expect(throws: DecodingError.self) {
try decoder.decode(UnitInterval.self, from: negativeJSON)
}
// Test value greater than 1
let tooLargeJSON = "1.5".data(using: .utf8)!
#expect(throws: DecodingError.self) {
try decoder.decode(UnitInterval.self, from: tooLargeJSON)
}
}
@Test("JSON encoding produces expected format")
func testJSONEncodingFormat() throws {
let encoder = JSONEncoder()
let half: UnitInterval = 0.5
let data = try encoder.encode(half)
let jsonString = String(data: data, encoding: .utf8)
#expect(jsonString == "0.5")
}
@Test("Double value property")
func testDoubleValueProperty() throws {
let values = [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0]
for value in values {
let computed = value * 1.0 // Force runtime computation
if let interval = UnitInterval(computed) {
#expect(interval.doubleValue == value)
} else {
#expect(Bool(false), "UnitInterval(\(value)) should succeed")
}
}
}
}