diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a13769..7b4c326 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,7 +56,38 @@ We require that your commit messages match our template. The easiest way to do t ### Run CI checks locally -You can run the Github Actions workflows locally using [act](https://github.com/nektos/act). For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally). +You can run the Github Actions workflows locally using +[act](https://github.com/nektos/act). To run all the jobs that run on a pull +request, use the following command: + +``` +% act pull_request +``` + +To run just a single job, use `workflow_call -j `, and specify the inputs +the job expects. For example, to run just shellcheck: + +``` +% act workflow_call -j soundness --input shell_check_enabled=true +``` + +To bind-mount the working directory to the container, rather than a copy, use +`--bind`. For example, to run just the formatting, and have the results +reflected in your working directory: + +``` +% act --bind workflow_call -j soundness --input format_check_enabled=true +``` + +If you'd like `act` to always run with certain flags, these can be be placed in +an `.actrc` file either in the current working directory or your home +directory, for example: + +``` +--container-architecture=linux/amd64 +--remote-name upstream +--action-offline-mode +``` ## How to contribute your work diff --git a/Tests/InMemoryTracingTests/InMemoryTracerTests.swift b/Tests/InMemoryTracingTests/InMemoryTracerTests.swift index 40b3709..a75f4a0 100644 --- a/Tests/InMemoryTracingTests/InMemoryTracerTests.swift +++ b/Tests/InMemoryTracingTests/InMemoryTracerTests.swift @@ -12,11 +12,10 @@ // //===----------------------------------------------------------------------===// -#if canImport(Testing) +@_spi(Testing) import InMemoryTracing @_spi(Locking) import Instrumentation import Testing import Tracing -@_spi(Testing) import InMemoryTracing @Suite("InMemoryTracer") struct InMemoryTracerTests { @@ -437,5 +436,3 @@ private final class MockClock { Instant(nanosecondsSinceEpoch: self._now) } } - -#endif diff --git a/Tests/InstrumentationTests/InstrumentTests.swift b/Tests/InstrumentationTests/InstrumentTests.swift index 9e74d51..0ba3fd7 100644 --- a/Tests/InstrumentationTests/InstrumentTests.swift +++ b/Tests/InstrumentationTests/InstrumentTests.swift @@ -12,12 +12,15 @@ // //===----------------------------------------------------------------------===// +import Foundation import Instrumentation import ServiceContextModule -import XCTest +import Testing -final class InstrumentTests: XCTestCase { - func testMultiplexInvokesAllInstruments() { +@Suite("MultiplexInstrument") +struct InstrumentTests { + @Test("MultiplexInstrument invokes all instruments") + func multiplexInvokesAllInstruments() { let instrument = MultiplexInstrument([ FirstFakeTracer(), SecondFakeTracer(), @@ -26,15 +29,14 @@ final class InstrumentTests: XCTestCase { var context = ServiceContext.topLevel instrument.extract([String: String](), into: &context, using: DictionaryExtractor()) - XCTAssertEqual(context[FirstFakeTracer.TraceIDKey.self], FirstFakeTracer.defaultTraceID) - XCTAssertEqual(context[SecondFakeTracer.TraceIDKey.self], SecondFakeTracer.defaultTraceID) + #expect(context[FirstFakeTracer.TraceIDKey.self] == FirstFakeTracer.defaultTraceID) + #expect(context[SecondFakeTracer.TraceIDKey.self] == SecondFakeTracer.defaultTraceID) var subsequentRequestHeaders = ["Accept": "application/json"] instrument.inject(context, into: &subsequentRequestHeaders, using: DictionaryInjector()) - XCTAssertEqual( - subsequentRequestHeaders, - [ + #expect( + subsequentRequestHeaders == [ "Accept": "application/json", FirstFakeTracer.headerName: FirstFakeTracer.defaultTraceID, SecondFakeTracer.headerName: SecondFakeTracer.defaultTraceID, diff --git a/Tests/TracingTests/ActorTracingTests.swift b/Tests/TracingTests/ActorTracingTests.swift index e84a9ac..3c7fa70 100644 --- a/Tests/TracingTests/ActorTracingTests.swift +++ b/Tests/TracingTests/ActorTracingTests.swift @@ -13,14 +13,19 @@ //===----------------------------------------------------------------------===// import ServiceContextModule +import Testing import Tracing -import XCTest @testable import Instrumentation /// This is a compile-time test -final class ActorTracingTests: XCTestCase { - func test() {} +@Suite("Actor Tracing Compatibility") +struct ActorTracingTests { + @Test("Compiles with actor isolation") + func compilesWithActorIsolation() { + // This test exists purely to verify that the code compiles + // with Swift's strict concurrency checking + } } func work() async {} diff --git a/Tests/TracingTests/DynamicTracepointTracerTests.swift b/Tests/TracingTests/DynamicTracepointTracerTests.swift index 0943948..1db601f 100644 --- a/Tests/TracingTests/DynamicTracepointTracerTests.swift +++ b/Tests/TracingTests/DynamicTracepointTracerTests.swift @@ -12,14 +12,17 @@ // //===----------------------------------------------------------------------===// +import Foundation import ServiceContextModule +import Testing import Tracing -import XCTest @testable import Instrumentation -final class DynamicTracepointTracerTests: XCTestCase { - func test_adhoc_enableBySourceLoc() { +@Suite("Dynamic Tracepoint Tracer Tests") +struct DynamicTracepointTracerTests { + @Test("Ad-hoc tracepoint enable by source location") + func adhoc_enableBySourceLoc() { let tracer = DynamicTracepointTestTracer() let fileID = #fileID @@ -49,16 +52,17 @@ final class DynamicTracepointTracerTests: XCTestCase { } } - XCTAssertEqual(tracer.spans.count, 4) + #expect(tracer.spans.count == 4) for span in tracer.spans { - XCTAssertEqual(span.context.traceID, "trace-id-fake-\(fileID)-\(fakeLine)") + #expect(span.context.traceID == "trace-id-fake-\(fileID)-\(fakeLine)") } - XCTAssertEqual(tracer.spans[0].context.spanID, "span-id-fake-\(fileID)-\(fakeLine)") - XCTAssertEqual(tracer.spans[1].context.spanID, "span-id-fake-\(fileID)-\(fakeNextLine)") + #expect(tracer.spans[0].context.spanID == "span-id-fake-\(fileID)-\(fakeLine)") + #expect(tracer.spans[1].context.spanID == "span-id-fake-\(fileID)-\(fakeNextLine)") } - func test_adhoc_enableByFunction() { + @Test("Ad-hoc tracepoint enable by function") + func adhoc_enableByFunction() { let tracer = DynamicTracepointTestTracer() let fileID = #fileID @@ -70,12 +74,12 @@ final class DynamicTracepointTracerTests: XCTestCase { self.logic(fakeLine: 55, tracer: tracer) self.traceMeLogic(fakeLine: fakeLine, tracer: tracer) - XCTAssertEqual(tracer.spans.count, 2) + #expect(tracer.spans.count == 2) for span in tracer.spans { - XCTAssertEqual(span.context.traceID, "trace-id-fake-\(fileID)-\(fakeLine)") + #expect(span.context.traceID == "trace-id-fake-\(fileID)-\(fakeLine)") } - XCTAssertEqual(tracer.spans[0].context.spanID, "span-id-fake-\(fileID)-\(fakeLine)") - XCTAssertEqual(tracer.spans[1].context.spanID, "span-id-fake-\(fileID)-\(fakeNextLine)") + #expect(tracer.spans[0].context.spanID == "span-id-fake-\(fileID)-\(fakeLine)") + #expect(tracer.spans[1].context.spanID == "span-id-fake-\(fileID)-\(fakeNextLine)") } func logic(fakeLine: UInt, tracer: any Tracer) { diff --git a/Tests/TracingTests/SpanTests.swift b/Tests/TracingTests/SpanTests.swift index 741bb9f..713d94c 100644 --- a/Tests/TracingTests/SpanTests.swift +++ b/Tests/TracingTests/SpanTests.swift @@ -13,75 +13,84 @@ //===----------------------------------------------------------------------===// import ServiceContextModule +import Testing import Tracing -import XCTest @testable import Instrumentation -final class SpanTests: XCTestCase { - func testSpanEventIsExpressibleByStringLiteral() { +@Suite("Span Tests") +struct SpanTests { + @Test("SpanEvent is ExpressibleByStringLiteral") + func spanEventIsExpressibleByStringLiteral() { let event: SpanEvent = "test" - XCTAssertEqual(event.name, "test") + #expect(event.name == "test") } - func testSpanEventUsesNanosecondsFromClock() { + @Test("SpanEvent uses nanoseconds from clock") + func spanEventUsesNanosecondsFromClock() { let clock = MockClock() clock.setTime(42_000_000) let event = SpanEvent(name: "test", at: clock.now) - XCTAssertEqual(event.name, "test") - XCTAssertEqual(event.nanosecondsSinceEpoch, 42_000_000) - XCTAssertEqual(event.millisecondsSinceEpoch, 42) + #expect(event.name == "test") + #expect(event.nanosecondsSinceEpoch == 42_000_000) + #expect(event.millisecondsSinceEpoch == 42) } - func testSpanAttributeIsExpressibleByStringLiteral() { + @Test("SpanAttribute is ExpressibleByStringLiteral") + func spanAttributeIsExpressibleByStringLiteral() { let stringAttribute: SpanAttribute = "test" guard case .string(let stringValue) = stringAttribute else { - XCTFail("Expected string attribute, got \(stringAttribute).") + Issue.record("Expected string attribute, got \(stringAttribute).") return } - XCTAssertEqual(stringValue, "test") + #expect(stringValue == "test") } - func testSpanAttributeIsExpressibleByStringInterpolation() { + @Test("SpanAttribute is ExpressibleByStringInterpolation") + func spanAttributeIsExpressibleByStringInterpolation() { let stringAttribute: SpanAttribute = "test \(true) \(42) \(3.14)" guard case .string(let stringValue) = stringAttribute else { - XCTFail("Expected string attribute, got \(stringAttribute).") + Issue.record("Expected string attribute, got \(stringAttribute).") return } - XCTAssertEqual(stringValue, "test true 42 3.14") + #expect(stringValue == "test true 42 3.14") } - func testSpanAttributeIsExpressibleByIntegerLiteral() { + @Test("SpanAttribute is ExpressibleByIntegerLiteral") + func spanAttributeIsExpressibleByIntegerLiteral() { let intAttribute: SpanAttribute = 42 guard case .int64(let intValue) = intAttribute else { - XCTFail("Expected int attribute, got \(intAttribute).") + Issue.record("Expected int attribute, got \(intAttribute).") return } - XCTAssertEqual(intValue, 42) + #expect(intValue == 42) } - func testSpanAttributeIsExpressibleByFloatLiteral() { + @Test("SpanAttribute is ExpressibleByFloatLiteral") + func spanAttributeIsExpressibleByFloatLiteral() { let doubleAttribute: SpanAttribute = 42.0 guard case .double(let doubleValue) = doubleAttribute else { - XCTFail("Expected float attribute, got \(doubleAttribute).") + Issue.record("Expected float attribute, got \(doubleAttribute).") return } - XCTAssertEqual(doubleValue, 42.0) + #expect(doubleValue == 42.0) } - func testSpanAttributeIsExpressibleByBooleanLiteral() { + @Test("SpanAttribute is ExpressibleByBooleanLiteral") + func spanAttributeIsExpressibleByBooleanLiteral() { let boolAttribute: SpanAttribute = false guard case .bool(let boolValue) = boolAttribute else { - XCTFail("Expected bool attribute, got \(boolAttribute).") + Issue.record("Expected bool attribute, got \(boolAttribute).") return } - XCTAssertFalse(boolValue) + #expect(boolValue == false) } - func testSpanAttributeIsExpressibleByArrayLiteral() { + @Test("SpanAttribute is ExpressibleByArrayLiteral") + func spanAttributeIsExpressibleByArrayLiteral() { let tracer = TestTracer() let s = tracer.startAnySpan("", context: .topLevel) s.attributes["hi"] = [42, 21] @@ -91,17 +100,19 @@ final class SpanTests: XCTestCase { s.attributes["hi"] = [1, 2, 34] } - func testSpanAttributeSetEntireCollection() { + @Test("SpanAttribute set entire collection") + func spanAttributeSetEntireCollection() { let tracer = TestTracer() let s = tracer.startAnySpan("", context: .topLevel) var attrs = s.attributes attrs["one"] = 42 attrs["two"] = [1, 2, 34] s.attributes = attrs - XCTAssertEqual(s.attributes["one"]?.toSpanAttribute(), SpanAttribute.int(42)) + #expect(s.attributes["one"]?.toSpanAttribute() == SpanAttribute.int(42)) } - func testSpanAttributesUX() { + @Test("SpanAttributes UX") + func spanAttributesUX() { var attributes: SpanAttributes = [:] // normally we can use just the span attribute values, and it is not type safe or guided in any way: @@ -112,37 +123,39 @@ final class SpanTests: XCTestCase { attributes["bools"] = [true, false, true] attributes["alive"] = false - XCTAssertEqual(attributes["thing.name"]?.toSpanAttribute(), SpanAttribute.string("hello")) - XCTAssertEqual(attributes["meaning.of.life"]?.toSpanAttribute(), SpanAttribute.int(42)) - XCTAssertEqual(attributes["alive"]?.toSpanAttribute(), SpanAttribute.bool(false)) + #expect(attributes["thing.name"]?.toSpanAttribute() == SpanAttribute.string("hello")) + #expect(attributes["meaning.of.life"]?.toSpanAttribute() == SpanAttribute.int(42)) + #expect(attributes["alive"]?.toSpanAttribute() == SpanAttribute.bool(false)) // An import like: `import TracingOpenTelemetrySupport` can enable type-safe well defined attributes attributes.name = "kappa" attributes.sampleHttp.statusCode = 200 attributes.sampleHttp.codesArray = [1, 2, 3] - XCTAssertEqual(attributes.name, SpanAttribute.string("kappa")) - XCTAssertEqual(attributes.name, "kappa") + #expect(attributes.name == SpanAttribute.string("kappa")) + #expect(attributes.name == "kappa") print("attributes", attributes) - XCTAssertEqual(attributes.sampleHttp.statusCode, 200) - XCTAssertEqual(attributes.sampleHttp.codesArray, [1, 2, 3]) + #expect(attributes.sampleHttp.statusCode == 200) + #expect(attributes.sampleHttp.codesArray == [1, 2, 3]) } - func testSpanAttributesCustomValue() { + @Test("SpanAttributes custom value") + func spanAttributesCustomValue() { var attributes: SpanAttributes = [:] // normally we can use just the span attribute values, and it is not type safe or guided in any way: attributes.sampleHttp.customType = CustomAttributeValue() - XCTAssertEqual( - attributes["http.custom_value"]?.toSpanAttribute(), - SpanAttribute.stringConvertible(CustomAttributeValue()) + #expect( + attributes["http.custom_value"]?.toSpanAttribute() + == SpanAttribute.stringConvertible(CustomAttributeValue()) ) - XCTAssertEqual(String(reflecting: attributes.sampleHttp.customType), "Optional(CustomAttributeValue())") - XCTAssertEqual(attributes.sampleHttp.customType, CustomAttributeValue()) + #expect(String(reflecting: attributes.sampleHttp.customType) == "Optional(CustomAttributeValue())") + #expect(attributes.sampleHttp.customType == CustomAttributeValue()) } - func testSpanAttributesAreIterable() { + @Test("SpanAttributes are iterable") + func spanAttributesAreIterable() { let attributes: SpanAttributes = [ "0": 0, "1": true, @@ -159,12 +172,13 @@ final class SpanTests: XCTestCase { guard case .some(.int64) = dictionary["0"], case .some(.bool) = dictionary["1"], case .some(.string) = dictionary["2"] else { - XCTFail("Expected all attributes to be copied to the dictionary.") + Issue.record("Expected all attributes to be copied to the dictionary.") return } } - func testSpanAttributesMerge() { + @Test("SpanAttributes merge") + func spanAttributesMerge() { var attributes: SpanAttributes = [ "0": 0, "1": true, @@ -178,13 +192,14 @@ final class SpanTests: XCTestCase { attributes.merge(other) - XCTAssertEqual(attributes["0"]?.toSpanAttribute(), 1) - XCTAssertEqual(attributes["1"]?.toSpanAttribute(), false) - XCTAssertEqual(attributes["2"]?.toSpanAttribute(), "test") - XCTAssertEqual(attributes["3"]?.toSpanAttribute(), "new") + #expect(attributes["0"]?.toSpanAttribute() == 1) + #expect(attributes["1"]?.toSpanAttribute() == false) + #expect(attributes["2"]?.toSpanAttribute() == "test") + #expect(attributes["3"]?.toSpanAttribute() == "new") } - func testSpanParentConvenience() { + @Test("Span parent convenience") + func spanParentConvenience() { var parentBaggage = ServiceContext.topLevel parentBaggage[TestBaggageContextKey.self] = "test" @@ -208,18 +223,19 @@ final class SpanTests: XCTestCase { attributes.sampleHttp.statusCode = 418 child.addLink(parent, attributes: attributes) - XCTAssertEqual(child.links.count, 1) - XCTAssertEqual(child.links[0].context[TestBaggageContextKey.self], "test") - XCTAssertEqual(child.links[0].attributes.sampleHttp.statusCode, 418) + #expect(child.links.count == 1) + #expect(child.links[0].context[TestBaggageContextKey.self] == "test") + #expect(child.links[0].attributes.sampleHttp.statusCode == 418) guard case .some(.int64(let statusCode)) = child.links[0].attributes["http.status_code"]?.toSpanAttribute() else { - XCTFail("Expected int value for http.status_code") + Issue.record("Expected int value for http.status_code") return } - XCTAssertEqual(statusCode, 418) + #expect(statusCode == 418) } - func testSpanAttributeSetterGetter() { + @Test("SpanAttribute setter/getter") + func spanAttributeSetterGetter() { var parentBaggage = ServiceContext.topLevel parentBaggage[TestBaggageContextKey.self] = "test" @@ -243,19 +259,20 @@ final class SpanTests: XCTestCase { attributes.set("http.status_code", value: .int32(418)) child.addLink(parent, attributes: attributes) - XCTAssertEqual(child.links.count, 1) - XCTAssertEqual(child.links[0].context[TestBaggageContextKey.self], "test") - XCTAssertEqual(child.links[0].attributes.sampleHttp.statusCode, 418) + #expect(child.links.count == 1) + #expect(child.links[0].context[TestBaggageContextKey.self] == "test") + #expect(child.links[0].attributes.sampleHttp.statusCode == 418) guard case .some(.int32(let statusCode)) = child.links[0].attributes["http.status_code"]?.toSpanAttribute() else { - XCTFail("Expected int value for http.status_code") + Issue.record("Expected int value for http.status_code") return } - XCTAssertEqual(statusCode, 418) - XCTAssertEqual(attributes.get("http.status_code"), SpanAttribute.int32(418)) + #expect(statusCode == 418) + #expect(attributes.get("http.status_code") == SpanAttribute.int32(418)) } - func testSpanUpdateAttributes() { + @Test("Span update attributes") + func spanUpdateAttributes() { let span = TestSpan( operationName: "client", startTime: DefaultTracerClock.now, @@ -268,8 +285,8 @@ final class SpanTests: XCTestCase { attributes.set("http.method", value: .string("GET")) } - XCTAssertEqual(span.attributes.get("http.status_code"), .int32(200)) - XCTAssertEqual(span.attributes.get("http.method"), .string("GET")) + #expect(span.attributes.get("http.status_code") == .int32(200)) + #expect(span.attributes.get("http.method") == .string("GET")) } } diff --git a/Tests/TracingTests/TracedLockTests.swift b/Tests/TracingTests/TracedLockTests.swift index 92d6a69..c2487d4 100644 --- a/Tests/TracingTests/TracedLockTests.swift +++ b/Tests/TracingTests/TracedLockTests.swift @@ -12,14 +12,17 @@ // //===----------------------------------------------------------------------===// +import Foundation import ServiceContextModule +import Testing import Tracing -import XCTest @testable import Instrumentation -final class TracedLockTests: XCTestCase { - func test_tracesLockedTime() { +@Suite("TracedLock") +struct TracedLockTests { + @Test("Traces locked time") + func tracesLockedTime() { let tracer = TracedLockPrintlnTracer() let lock = TracedLock(name: "my-cool-lock") diff --git a/Tests/TracingTests/TracerTests.swift b/Tests/TracingTests/TracerTests.swift index bd8eba8..222b66a 100644 --- a/Tests/TracingTests/TracerTests.swift +++ b/Tests/TracingTests/TracerTests.swift @@ -13,8 +13,8 @@ //===----------------------------------------------------------------------===// import ServiceContextModule +import Testing import Tracing -import XCTest @testable @_spi(Locking) import Instrumentation @@ -22,8 +22,10 @@ import XCTest @preconcurrency import Dispatch #endif -final class TracerTests: XCTestCase { - func testContextPropagation() { +@Suite("Tracer Tests") +struct TracerTests { + @Test("Context propagation") + func contextPropagation() { let tracer = TestTracer() let httpServer = FakeHTTPServer { context, _, client -> FakeHTTPResponse in client.performRequest(context, request: FakeHTTPRequest(path: "/test", headers: []), tracer: tracer) @@ -32,13 +34,14 @@ final class TracerTests: XCTestCase { httpServer.receive(FakeHTTPRequest(path: "/", headers: [("trace-id", "test")]), tracer: tracer) - XCTAssertEqual(tracer.spans.count, 2) + #expect(tracer.spans.count == 2) for span in tracer.spans { - XCTAssertEqual(span.context.traceID, "test") + #expect(span.context.traceID == "test") } } - func testContextPropagationWithNoOpSpan() { + @Test("Context propagation with NoOp span") + func contextPropagationWithNoOpSpan() { let tracer = TestTracer() let httpServer = FakeHTTPServer { _, _, client -> FakeHTTPResponse in var context = ServiceContext.topLevel @@ -49,11 +52,12 @@ final class TracerTests: XCTestCase { httpServer.receive(FakeHTTPRequest(path: "/", headers: [("trace-id", "test")]), tracer: tracer) - XCTAssertEqual(httpServer.client.contexts.count, 1) - XCTAssertEqual(httpServer.client.contexts.first?.traceID, "test") + #expect(httpServer.client.contexts.count == 1) + #expect(httpServer.client.contexts.first?.traceID == "test") } - func testWithSpan_success() { + @Test("withSpan success") + func withSpan_success() { let tracer = TestTracer() var spanEnded = false @@ -65,11 +69,12 @@ final class TracerTests: XCTestCase { "yes" } - XCTAssertEqual(value, "yes") - XCTAssertTrue(spanEnded) + #expect(value == "yes") + #expect(spanEnded == true) } - func testWithSpan_throws() { + @Test("withSpan throws") + func withSpan_throws() { let tracer = TestTracer() var spanEnded = false @@ -80,14 +85,15 @@ final class TracerTests: XCTestCase { throw ExampleSpanError() } } catch { - XCTAssertTrue(spanEnded) - XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + #expect(spanEnded == true) + #expect(error as? ExampleSpanError == ExampleSpanError()) return } - XCTFail("Should have thrown") + Issue.record("Should have thrown") } - func testWithSpan_automaticBaggagePropagation_sync() throws { + @Test("withSpan automatic context propagation (sync)") + func withSpan_automaticBaggagePropagation_sync() throws { let tracer = TestTracer() var spanEnded = false @@ -98,15 +104,16 @@ final class TracerTests: XCTestCase { } let value = tracer.withAnySpan("hello") { (span: any Tracing.Span) -> String in - XCTAssertEqual(span.context.traceID, ServiceContext.current?.traceID) + #expect(span.context.traceID == ServiceContext.current?.traceID) return operation(span: span) } - XCTAssertEqual(value, "world") - XCTAssertTrue(spanEnded) + #expect(value == "world") + #expect(spanEnded == true) } - func testWithSpan_automaticBaggagePropagation_sync_throws() throws { + @Test("withSpan automatic context propagation (sync, throws)") + func withSpan_automaticBaggagePropagation_sync_throws() throws { let tracer = TestTracer() var spanEnded = false @@ -119,14 +126,15 @@ final class TracerTests: XCTestCase { do { _ = try tracer.withAnySpan("hello", operation) } catch { - XCTAssertTrue(spanEnded) - XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + #expect(spanEnded == true) + #expect(error as? ExampleSpanError == ExampleSpanError()) return } - XCTFail("Should have thrown") + Issue.record("Should have thrown") } - func testWithSpan_automaticBaggagePropagation_async() async throws { + @Test("withSpan automatic context propagation (async)") + func withSpan_automaticBaggagePropagation_async() async throws { let tracer = TestTracer() let spanEnded: LockedValueBox = .init(false) @@ -137,15 +145,16 @@ final class TracerTests: XCTestCase { } let value = try await tracer.withAnySpan("hello") { (span: any Tracing.Span) -> String in - XCTAssertEqual(span.context.traceID, ServiceContext.current?.traceID) + #expect(span.context.traceID == ServiceContext.current?.traceID) return try await operation(span) } - XCTAssertEqual(value, "world") - XCTAssertTrue(spanEnded.withValue { $0 }) + #expect(value == "world") + #expect(spanEnded.withValue { $0 } == true) } - func testWithSpan_enterFromNonAsyncCode_passBaggage_asyncOperation() async throws { + @Test("withSpan from non-async code with async operation") + func withSpan_enterFromNonAsyncCode_passBaggage_asyncOperation() async throws { let tracer = TestTracer() let spanEnded: LockedValueBox = .init(false) @@ -159,16 +168,17 @@ final class TracerTests: XCTestCase { fromNonAsyncWorld.traceID = "1234-5678" let value = await tracer.withAnySpan("hello", context: fromNonAsyncWorld) { (span: any Tracing.Span) -> String in - XCTAssertEqual(span.context.traceID, ServiceContext.current?.traceID) - XCTAssertEqual(span.context.traceID, fromNonAsyncWorld.traceID) + #expect(span.context.traceID == ServiceContext.current?.traceID) + #expect(span.context.traceID == fromNonAsyncWorld.traceID) return await operation(span) } - XCTAssertEqual(value, "world") - XCTAssertTrue(spanEnded.withValue { $0 }) + #expect(value == "world") + #expect(spanEnded.withValue { $0 } == true) } - func testWithSpan_automaticBaggagePropagation_async_throws() async throws { + @Test("withSpan automatic context propagation (async, throws)") + func withSpan_automaticBaggagePropagation_async_throws() async throws { let tracer = TestTracer() let spanEnded: LockedValueBox = .init(false) @@ -181,14 +191,15 @@ final class TracerTests: XCTestCase { do { _ = try await tracer.withAnySpan("hello", operation) } catch { - XCTAssertTrue(spanEnded.withValue { $0 }) - XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + #expect(spanEnded.withValue { $0 } == true) + #expect(error as? ExampleSpanError == ExampleSpanError()) return } - XCTFail("Should have thrown") + Issue.record("Should have thrown") } - func test_static_Tracer_withSpan_automaticBaggagePropagation_async_throws() async throws { + @Test("Static Tracer.withSpan automatic context propagation (async, throws)") + func static_Tracer_withSpan_automaticBaggagePropagation_async_throws() async throws { let tracer = TestTracer() let spanEnded: LockedValueBox = .init(false) @@ -201,14 +212,15 @@ final class TracerTests: XCTestCase { do { _ = try await tracer.withSpan("hello", operation) } catch { - XCTAssertTrue(spanEnded.withValue { $0 }) - XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + #expect(spanEnded.withValue { $0 } == true) + #expect(error as? ExampleSpanError == ExampleSpanError()) return } - XCTFail("Should have thrown") + Issue.record("Should have thrown") } - func test_static_Tracer_withSpan_automaticBaggagePropagation_throws() async throws { + @Test("Static Tracer.withSpan automatic context propagation (throws)") + func static_Tracer_withSpan_automaticBaggagePropagation_throws() async throws { let tracer = TestTracer() let spanEnded: LockedValueBox = .init(false) @@ -221,14 +233,15 @@ final class TracerTests: XCTestCase { do { _ = try await tracer.withSpan("hello", operation) } catch { - XCTAssertTrue(spanEnded.withValue { $0 }) - XCTAssertEqual(error as? ExampleSpanError, ExampleSpanError()) + #expect(spanEnded.withValue { $0 } == true) + #expect(error as? ExampleSpanError == ExampleSpanError()) return } - XCTFail("Should have thrown") + Issue.record("Should have thrown") } - func testWithSpan_recordErrorWithAttributes() throws { + @Test("withSpan record error with attributes") + func withSpan_recordErrorWithAttributes() throws { let tracer = TestTracer() var endedSpan: TestSpan? @@ -241,15 +254,16 @@ final class TracerTests: XCTestCase { span.recordError(errorToThrow, attributes: attrsForError) } - XCTAssertTrue(endedSpan != nil) - XCTAssertEqual(endedSpan!.recordedErrors.count, 1) + #expect(endedSpan != nil) + #expect(endedSpan!.recordedErrors.count == 1) let error = endedSpan!.recordedErrors.first!.0 - XCTAssertEqual(error as! ExampleSpanError, errorToThrow) + #expect(error as! ExampleSpanError == errorToThrow) let attrs = endedSpan!.recordedErrors.first!.1 - XCTAssertEqual(attrs, attrsForError) + #expect(attrs == attrsForError) } - func testWithSpanSignatures() { + @Test("withSpan signatures") + func withSpanSignatures() { let tracer = TestTracer() let clock = DefaultTracerClock() @@ -262,7 +276,8 @@ final class TracerTests: XCTestCase { tracer.withAnySpan("", context: .topLevel) { _ in } } - func testWithSpanShouldNotMissPropagatingInstant() { + @Test("withSpan should not miss propagating instant") + func withSpanShouldNotMissPropagatingInstant() { let tracer = TestTracer() let clock = DefaultTracerClock() @@ -271,7 +286,7 @@ final class TracerTests: XCTestCase { tracer.withSpan("span", at: instant) { _ in } let span = tracer.spans.first! - XCTAssertEqual(span.startTimestampNanosSinceEpoch, instant.nanosecondsSinceEpoch) + #expect(span.startTimestampNanosSinceEpoch == instant.nanosecondsSinceEpoch) } } diff --git a/Tests/TracingTests/TracerTimeTests.swift b/Tests/TracingTests/TracerTimeTests.swift index d0eae16..83bf3bd 100644 --- a/Tests/TracingTests/TracerTimeTests.swift +++ b/Tests/TracingTests/TracerTimeTests.swift @@ -12,31 +12,32 @@ // //===----------------------------------------------------------------------===// +import Testing import Tracing -import XCTest import struct Foundation.Date @testable import Instrumentation -final class TracerTimeTests: XCTestCase { - func testTracerTime() { +@Suite("Tracer Time") +struct TracerTimeTests { + @Test("DefaultTracerClock matches Date") + func defaultTracerClockMatchesDate() { let t = DefaultTracerClock.now let d = Date() - XCTAssertEqual( - Double(t.millisecondsSinceEpoch) / 1000, // seconds - d.timeIntervalSince1970, // seconds - accuracy: 10 + #expect( + abs(Double(t.millisecondsSinceEpoch) / 1000 - d.timeIntervalSince1970) < 10 ) } - func testMockTimeStartSpan() { + @Test("Mock time with startSpan") + func mockTimeStartSpan() { let tracer = TestTracer() let mockClock = MockClock() mockClock.setTime(13) let span: TestSpan = tracer.startSpan("start", at: mockClock.now) - XCTAssertEqual(span.startTimestampNanosSinceEpoch, 13) + #expect(span.startTimestampNanosSinceEpoch == 13) } } diff --git a/Tests/TracingTests/TracingInstrumentationSystemTests.swift b/Tests/TracingTests/TracingInstrumentationSystemTests.swift index ede6a3c..98994b7 100644 --- a/Tests/TracingTests/TracingInstrumentationSystemTests.swift +++ b/Tests/TracingTests/TracingInstrumentationSystemTests.swift @@ -12,67 +12,68 @@ // //===----------------------------------------------------------------------===// +import Testing import Tracing -import XCTest @testable import Instrumentation extension InstrumentationSystem { - public static func _legacyTracer(of tracerType: T.Type) -> T? where T: LegacyTracer { + fileprivate static func _legacyTracer(of tracerType: T.Type) -> T? where T: LegacyTracer { self._findInstrument(where: { $0 is T }) as? T } - public static func _tracer(of tracerType: T.Type) -> T? where T: Tracer { + fileprivate static func _tracer(of tracerType: T.Type) -> T? where T: Tracer { self._findInstrument(where: { $0 is T }) as? T } - public static func _instrument(of instrumentType: I.Type) -> I? where I: Instrument { + fileprivate static func _instrument(of instrumentType: I.Type) -> I? where I: Instrument { self._findInstrument(where: { $0 is I }) as? I } } -/// This is the only test relying in the global InstrumentationSystem -final class GlobalTracingInstrumentationSystemTests: XCTestCase { - override class func tearDown() { - super.tearDown() +/// Tests that rely on the global InstrumentationSystem +/// These tests must be isolated from each other since they mutate global state +@Suite("Global InstrumentationSystem", .serialized) +struct GlobalTracingInstrumentationSystemTests { + + @Test("Provides access to a tracer") + func accessToTracer() { + // Clean state before test InstrumentationSystem.bootstrapInternal(nil) - } + defer { InstrumentationSystem.bootstrapInternal(nil) } - func testItProvidesAccessToATracer() { let tracer = TestTracer() - XCTAssertNil(InstrumentationSystem._legacyTracer(of: TestTracer.self)) - XCTAssertNil(InstrumentationSystem._tracer(of: TestTracer.self)) + #expect(InstrumentationSystem._legacyTracer(of: TestTracer.self) == nil) + #expect(InstrumentationSystem._tracer(of: TestTracer.self) == nil) InstrumentationSystem.bootstrapInternal(tracer) - XCTAssertFalse(InstrumentationSystem.instrument is MultiplexInstrument) - XCTAssert(InstrumentationSystem._instrument(of: TestTracer.self) === tracer) - XCTAssertNil(InstrumentationSystem._instrument(of: NoOpInstrument.self)) + #expect(InstrumentationSystem.instrument is MultiplexInstrument == false) + #expect(InstrumentationSystem._instrument(of: TestTracer.self) === tracer) + #expect(InstrumentationSystem._instrument(of: NoOpInstrument.self) == nil) - XCTAssert(InstrumentationSystem._legacyTracer(of: TestTracer.self) === tracer) - XCTAssert(InstrumentationSystem.legacyTracer is TestTracer) - XCTAssert(InstrumentationSystem._tracer(of: TestTracer.self) === tracer) - XCTAssert(InstrumentationSystem.tracer is TestTracer) + #expect(InstrumentationSystem._legacyTracer(of: TestTracer.self) === tracer) + #expect(InstrumentationSystem.legacyTracer is TestTracer) + #expect(InstrumentationSystem._tracer(of: TestTracer.self) === tracer) + #expect(InstrumentationSystem.tracer is TestTracer) let multiplexInstrument = MultiplexInstrument([tracer]) InstrumentationSystem.bootstrapInternal(multiplexInstrument) - XCTAssert(InstrumentationSystem.instrument is MultiplexInstrument) - XCTAssert(InstrumentationSystem._instrument(of: TestTracer.self) === tracer) + #expect(InstrumentationSystem.instrument is MultiplexInstrument) + #expect(InstrumentationSystem._instrument(of: TestTracer.self) === tracer) - XCTAssert(InstrumentationSystem._legacyTracer(of: TestTracer.self) === tracer) - XCTAssert(InstrumentationSystem.legacyTracer is TestTracer) - XCTAssert(InstrumentationSystem._tracer(of: TestTracer.self) === tracer) - XCTAssert(InstrumentationSystem.tracer is TestTracer) + #expect(InstrumentationSystem._legacyTracer(of: TestTracer.self) === tracer) + #expect(InstrumentationSystem.legacyTracer is TestTracer) + #expect(InstrumentationSystem._tracer(of: TestTracer.self) === tracer) + #expect(InstrumentationSystem.tracer is TestTracer) } -} -final class GlobalTracingMethodsTests: XCTestCase { - override class func tearDown() { - super.tearDown() + @Test("Global tracing methods preserve arguments") + func globalTracingMethods() async throws { + // Clean state before test InstrumentationSystem.bootstrapInternal(nil) - } + defer { InstrumentationSystem.bootstrapInternal(nil) } - func testGlobalTracingMethods() async { // Bootstrap with TestTracer to capture spans let tracer = TestTracer() InstrumentationSystem.bootstrapInternal(tracer) @@ -123,10 +124,10 @@ final class GlobalTracingMethodsTests: XCTestCase { context: customContext1, ofKind: .consumer ) { span -> String in - XCTAssertEqual(span.operationName, "withSpan-sync-instant") + #expect(span.operationName == "withSpan-sync-instant") return "sync-result" } - XCTAssertEqual(result1, "sync-result") + #expect(result1 == "sync-result") // Test 5: withSpan synchronous without instant let result2 = withSpan( @@ -134,10 +135,10 @@ final class GlobalTracingMethodsTests: XCTestCase { context: customContext2, ofKind: .internal ) { span -> Int in - XCTAssertEqual(span.operationName, "withSpan-sync-default") + #expect(span.operationName == "withSpan-sync-default") return 42 } - XCTAssertEqual(result2, 42) + #expect(result2 == 42) // Test 6: withSpan synchronous with instant (alt parameter order) let result3 = withSpan( @@ -148,7 +149,7 @@ final class GlobalTracingMethodsTests: XCTestCase { ) { _ in "alt-result" } - XCTAssertEqual(result3, "alt-result") + #expect(result3 == "alt-result") // Test 7: withSpan async with custom instant and isolation let result4 = await withSpan( @@ -158,10 +159,10 @@ final class GlobalTracingMethodsTests: XCTestCase { ofKind: .client, isolation: nil ) { span -> String in - XCTAssertEqual(span.operationName, "withSpan-async-instant-isolation") + #expect(span.operationName == "withSpan-async-instant-isolation") return "async-result" } - XCTAssertEqual(result4, "async-result") + #expect(result4 == "async-result") // Test 8: withSpan async without instant but with isolation let result5 = await withSpan( @@ -170,10 +171,10 @@ final class GlobalTracingMethodsTests: XCTestCase { ofKind: .producer, isolation: nil ) { span -> Bool in - XCTAssertEqual(span.operationName, "withSpan-async-default-isolation") + #expect(span.operationName == "withSpan-async-default-isolation") return true } - XCTAssertEqual(result5, true) + #expect(result5 == true) // Test 9: withSpan async with instant, isolation (alt parameter order) let result6 = await withSpan( @@ -185,66 +186,66 @@ final class GlobalTracingMethodsTests: XCTestCase { ) { _ in 99 } - XCTAssertEqual(result6, 99) + #expect(result6 == 99) // Verify all spans were recorded with correct properties let finishedSpans = tracer.spans - XCTAssertEqual(finishedSpans.count, 9, "Expected 9 finished spans") + #expect(finishedSpans.count == 9) // Verify span 1: startSpan with custom instant let recorded1 = finishedSpans[0] - XCTAssertEqual(recorded1.operationName, "startSpan-with-instant") - XCTAssertEqual(recorded1.kind, .client) - XCTAssertEqual(recorded1.context[TestContextKey.self], "context1") - XCTAssertEqual(recorded1.startTimestampNanosSinceEpoch, customInstant1.nanosecondsSinceEpoch) + #expect(recorded1.operationName == "startSpan-with-instant") + #expect(recorded1.kind == .client) + #expect(recorded1.context[TestContextKey.self] == "context1") + #expect(recorded1.startTimestampNanosSinceEpoch == customInstant1.nanosecondsSinceEpoch) // Verify span 2: startSpan without instant let recorded2 = finishedSpans[1] - XCTAssertEqual(recorded2.operationName, "startSpan-default-instant") - XCTAssertEqual(recorded2.kind, .server) - XCTAssertEqual(recorded2.context[TestContextKey.self], "context2") + #expect(recorded2.operationName == "startSpan-default-instant") + #expect(recorded2.kind == .server) + #expect(recorded2.context[TestContextKey.self] == "context2") // Note: Can't verify exact instant since it used DefaultTracerClock.now // Verify span 3: startSpan with instant (alt) let recorded3 = finishedSpans[2] - XCTAssertEqual(recorded3.operationName, "startSpan-instant-alt") - XCTAssertEqual(recorded3.kind, .producer) - XCTAssertEqual(recorded3.startTimestampNanosSinceEpoch, customInstant2.nanosecondsSinceEpoch) + #expect(recorded3.operationName == "startSpan-instant-alt") + #expect(recorded3.kind == .producer) + #expect(recorded3.startTimestampNanosSinceEpoch == customInstant2.nanosecondsSinceEpoch) // Verify span 4: withSpan sync with instant let recorded4 = finishedSpans[3] - XCTAssertEqual(recorded4.operationName, "withSpan-sync-instant") - XCTAssertEqual(recorded4.kind, .consumer) - XCTAssertEqual(recorded4.context[TestContextKey.self], "context1") - XCTAssertEqual(recorded4.startTimestampNanosSinceEpoch, customInstant3.nanosecondsSinceEpoch) + #expect(recorded4.operationName == "withSpan-sync-instant") + #expect(recorded4.kind == .consumer) + #expect(recorded4.context[TestContextKey.self] == "context1") + #expect(recorded4.startTimestampNanosSinceEpoch == customInstant3.nanosecondsSinceEpoch) // Verify span 5: withSpan sync without instant let recorded5 = finishedSpans[4] - XCTAssertEqual(recorded5.operationName, "withSpan-sync-default") - XCTAssertEqual(recorded5.kind, .internal) - XCTAssertEqual(recorded5.context[TestContextKey.self], "context2") + #expect(recorded5.operationName == "withSpan-sync-default") + #expect(recorded5.kind == .internal) + #expect(recorded5.context[TestContextKey.self] == "context2") // Verify span 6: withSpan sync with instant (alt) let recorded6 = finishedSpans[5] - XCTAssertEqual(recorded6.operationName, "withSpan-sync-instant-alt") - XCTAssertEqual(recorded6.kind, .server) + #expect(recorded6.operationName == "withSpan-sync-instant-alt") + #expect(recorded6.kind == .server) // Verify span 7: withSpan async with instant and isolation let recorded7 = finishedSpans[6] - XCTAssertEqual(recorded7.operationName, "withSpan-async-instant-isolation") - XCTAssertEqual(recorded7.kind, .client) - XCTAssertEqual(recorded7.context[TestContextKey.self], "context1") + #expect(recorded7.operationName == "withSpan-async-instant-isolation") + #expect(recorded7.kind == .client) + #expect(recorded7.context[TestContextKey.self] == "context1") // Verify span 8: withSpan async without instant but with isolation let recorded8 = finishedSpans[7] - XCTAssertEqual(recorded8.operationName, "withSpan-async-default-isolation") - XCTAssertEqual(recorded8.kind, .producer) - XCTAssertEqual(recorded8.context[TestContextKey.self], "context2") + #expect(recorded8.operationName == "withSpan-async-default-isolation") + #expect(recorded8.kind == .producer) + #expect(recorded8.context[TestContextKey.self] == "context2") // Verify span 9: withSpan async with instant and isolation (alt) let recorded9 = finishedSpans[8] - XCTAssertEqual(recorded9.operationName, "withSpan-async-instant-isolation-alt") - XCTAssertEqual(recorded9.kind, .consumer) + #expect(recorded9.operationName == "withSpan-async-instant-isolation-alt") + #expect(recorded9.kind == .consumer) } }