Skip to content
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

RUMM-1890 Fix tests flakiness #711

Merged
merged 7 commits into from
Jan 11, 2022
10 changes: 7 additions & 3 deletions Sources/Datadog/Core/System/Time/ServerDateProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,21 @@ internal class NTPServerDateProvider: ServerDateProvider {
self?.publisher.publishAsync(offset)
},
completion: { [weak self] now, offset in
guard let self = self else {
return
}

// Kronos only notifies for the first and last samples.
// In case, the last sample does not return an offset, we calculate the offset
// from the returned `now` parameter. The `now` parameter in this callback
// is `Clock.now`, so it is possible to have `now` but not `offset`.
if let offset = offset {
self?.publisher.publishAsync(offset)
self.publisher.publishAsync(offset)
} else if let now = now {
self?.publisher.publishAsync(now.timeIntervalSinceNow)
self.publisher.publishAsync(now.timeIntervalSinceNow)
}

completion(self?.publisher.currentValue)
completion(self.publisher.currentValue)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class CrashContextProviderTests: XCTestCase {

// MARK: - `NetworkConnectionInfo` Integration

func testWhenCurrentValueIsObtainedFromNetworkConnectionInfoProvider_thenCrashContextProviderNotifiesNewContext() {
func testWhenCurrentValueIsObtainedFromNetworkConnectionInfoProvider_thenCrashContextProviderNotifiesNewContext() throws {
let expectation = self.expectation(description: "Notify new crash context")
let initialNetworkConnectionInfo: NetworkConnectionInfo = .mockRandom()
let wrappedProvider = NetworkConnectionInfoProviderMock(networkConnectionInfo: initialNetworkConnectionInfo)
Expand Down Expand Up @@ -188,8 +188,9 @@ class CrashContextProviderTests: XCTestCase {

// Then
waitForExpectations(timeout: 1, handler: nil)
XCTAssertEqual(initialContext.lastNetworkConnectionInfo, initialNetworkConnectionInfo)
XCTAssertEqual(updatedContext?.lastNetworkConnectionInfo, currentNetworkConnectionInfo)
let updatedNetworkConnectionInfo = try XCTUnwrap(updatedContext?.lastNetworkConnectionInfo)
XCTAssertEqual(initialContext.lastNetworkConnectionInfo, initialNetworkConnectionInfo, "It must store initial network info")
XCTAssertEqual(updatedNetworkConnectionInfo, currentNetworkConnectionInfo, "It must store updated network info")
Comment on lines -191 to +193
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It failed 2 times over 1 month, but no clue on what's happening wrong in this test. I'm just adding more verbosity to these assertions, so we can better understand it.

}

// MARK: - `CarrierInfo` Integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class RUMApplicationScopeTests: XCTestCase {
backgroundEventTrackingEnabled: .mockAny()
)

let simulatedSessionsCount = 200
let simulatedSessionsCount = 400
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using tests repetition in local:

  • with 200 samples - 4 failures in 500 runs
  • with 300 samples - 1 failures in 500 runs
  • with 400 samples - 0 failures in 500 runs

With 400 samples it runs in ~180ms.

(0..<simulatedSessionsCount).forEach { _ in
_ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: mockView))
_ = scope.process(command: RUMStopViewCommand.mockWith(time: currentTime, identity: mockView))
Expand All @@ -161,7 +161,8 @@ class RUMApplicationScopeTests: XCTestCase {
let viewEventsCount = try output.recordedEvents(ofType: RUMEvent<RUMViewEvent>.self).count
let trackedSessionsCount = Double(viewEventsCount) / 2 // each Session should send 2 View updates

XCTAssertGreaterThan(trackedSessionsCount, 100 * 0.8) // -20%
XCTAssertLessThan(trackedSessionsCount, 100 * 1.2) // +20%
let halfSessionsCount = 0.5 * Double(simulatedSessionsCount)
XCTAssertGreaterThan(trackedSessionsCount, halfSessionsCount * 0.8) // -20%
XCTAssertLessThan(trackedSessionsCount, halfSessionsCount * 1.2) // +20%
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,15 @@ class RUMViewIdentityTests: XCTestCase {
// MARK: - Memory management

func testItStoresWeakReferenceToUIViewController() throws {
var vc: UIViewController? = UIViewController()
var identity: RUMViewIdentity! // swiftlint:disable:this implicitly_unwrapped_optional

let identity = try XCTUnwrap(vc?.asRUMViewIdentity())
try autoreleasepool {
var vc: UIViewController? = UIViewController()
identity = try XCTUnwrap(vc?.asRUMViewIdentity())
XCTAssertNotNil(identity.identifiable, "Reference should be available while `vc` is alive.")
vc = nil
}

XCTAssertNotNil(identity.identifiable, "Reference should be available while `vc` is alive.")
vc = nil
XCTAssertNil(identity.identifiable, "Reference should not be available after `vc` was deallocated.")
Comment on lines -94 to 101
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 failures in last 1 month. It was failing on:

XCTAssertNotNil(identity.identifiable, "Reference should be available while `vc` is alive.")

I assume that autoreleasepool {} should help as VC is autorelease Objective-C object and now we clean it up in different scope than the assertion.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,54 @@ import UIKit
class VitalRefreshRateReaderTests: XCTestCase {
private let mockNotificationCenter = NotificationCenter()

func testHighAndLowRefreshRates() {
func testWhenMainThreadOverheadGoesUp_itMeasuresLowerRefreshRate() throws {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 failures in last 1 month. This is quite old issue, I changed the approach for this test:

  • Instead of observing main thread for certain amount of time...
  • ... now it observes it until VitalRefreshRateReader records certain number of samples.

It passes 500x repetition in local. From my observation, time-based constraint was very flaky (sometimes recording just 1 sample, sometimes many). Current approach of recording 30 samples seems stable and still executes under 2s in local (like before).

let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter)
let registrar_view1 = VitalPublisher(initialValue: VitalInfo())
let registrar_view2 = VitalPublisher(initialValue: VitalInfo())

// View1 has simple UI, high FPS expected
reader.register(registrar_view1)

// Wait without blocking UI thread
let expectation1 = expectation(description: "async expectation for first observer")
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
expectation1.fulfill()
let targetSamplesCount = 30

/// Runs given work on the main thread until `condition` is met, then calls `completion`.
func run(mainThreadWork: @escaping () -> Void, until condition: @escaping () -> Bool, completion: @escaping () -> Void) {
if !condition() {
mainThreadWork()
DispatchQueue.main.async { // schedule to next runloop
run(mainThreadWork: mainThreadWork, until: condition, completion: completion)
}
} else {
completion()
}
}

waitForExpectations(timeout: 1.0) { _ in
XCTAssertGreaterThan(registrar_view1.currentValue.sampleCount, 0)
XCTAssertGreaterThan(UIScreen.main.maximumFramesPerSecond, Int(registrar_view1.currentValue.maxValue!))
XCTAssertGreaterThan(registrar_view1.currentValue.minValue!, 0.0)
/// Records `targetSamplesCount` samples into `measure` by running given work on the main thread.
func record(_ measure: VitalPublisher, mainThreadWork: @escaping () -> Void) {
let completion = expectation(description: "Complete measurement")
reader.register(measure)

run(
mainThreadWork: mainThreadWork,
until: { measure.currentValue.sampleCount == targetSamplesCount },
completion: {
reader.unregister(measure)
completion.fulfill()
}
)

wait(for: [completion], timeout: 10)
}

reader.unregister(registrar_view1)
// Given
let lowOverhead = { /* no-op */ } // no overhead in succeeding runloop runs
let lowOverheadMeasure = VitalPublisher(initialValue: VitalInfo())

// View2 has complex UI, lower FPS expected
reader.register(registrar_view2)
let highOverhead = { Thread.sleep(forTimeInterval: 0.02) } // 0.02 overhead in succeeding runloop runs
let highOverheadMeasure = VitalPublisher(initialValue: VitalInfo())

// Block UI thread
Thread.sleep(forTimeInterval: 1.0)

// Wait after blocking UI thread so that reader will read refresh rate before assertions
let expectation2 = expectation(description: "async expectation for second observer")
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
expectation2.fulfill()
}
waitForExpectations(timeout: 0.5) { _ in }
// When
record(lowOverheadMeasure, mainThreadWork: lowOverhead)
record(highOverheadMeasure, mainThreadWork: highOverhead)

XCTAssertGreaterThan(registrar_view2.currentValue.sampleCount, 0)
XCTAssertGreaterThan(registrar_view1.currentValue.meanValue!, registrar_view2.currentValue.meanValue!)
// Then
let expectedHighFPS = try XCTUnwrap(lowOverheadMeasure.currentValue.meanValue)
let expectedLowFPS = try XCTUnwrap(highOverheadMeasure.currentValue.meanValue)
XCTAssertGreaterThan(expectedHighFPS, expectedLowFPS, "It must measure higher FPS for lower main thread overhead")
}

func testAppStateHandling() {
Expand Down
23 changes: 17 additions & 6 deletions Tests/DatadogTests/Helpers/XCTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,23 @@ extension XCTestCase {
) throws {
let prettyEncoder = JSONEncoder()
prettyEncoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let encodedValue1 = try prettyEncoder.encode(value1)
let encodedValue2 = try prettyEncoder.encode(value2)

let value1JSONString = encodedValue1.utf8String
let value2JSONString = encodedValue2.utf8String
XCTAssertEqual(value1JSONString, value2JSONString, file: file, line: line)
do {
let encodedValue1: Data
let encodedValue2: Data
if #available(iOS 13.0, *) {
encodedValue1 = try prettyEncoder.encode(value1)
encodedValue2 = try prettyEncoder.encode(value2)
} else {
encodedValue1 = try prettyEncoder.encode(EncodingContainer(value1))
encodedValue2 = try prettyEncoder.encode(EncodingContainer(value2))
}
Comment on lines +152 to +158
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the biggest problem in last 1 month - 14 failures in:

func testGivenContextWithLastRUMSessionStateSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws {
let randomRUMSessionState: RUMSessionState? = Bool.random() ? .mockRandom() : nil
// Given
var context: CrashContext = .mockRandom()
context.lastRUMSessionState = randomRUMSessionState
// When
let serializedContext = try encoder.encode(context)
// Then
let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext)
try AssertEncodedRepresentationsEqual(
value1: deserializedContext.lastRUMSessionState,
value2: randomRUMSessionState
)
}

When fuzzy input is resolved to nil, serialization fails below iOS 13. To fix it I'm using the helper we added exactly for this case:

/// Prior to `iOS13.0`, the `JSONEncoder` supports only object or array as the root type.
/// Hence we can't test encoding `Encodable` values directly and we need to wrap it inside this `EncodingContainer` container.
///
/// Reference: https://bugs.swift.org/browse/SR-6163
struct EncodingContainer<Value: Encodable>: Encodable {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

let value1JSONString = encodedValue1.utf8String
let value2JSONString = encodedValue2.utf8String
XCTAssertEqual(value1JSONString, value2JSONString, file: file, line: line)
} catch let exception {
XCTFail("Failed to encode one of the values in `AssertEncodedRepresentationsEqual`", file: file, line: line)
throw exception
}
}

func AssertURLSessionTasksIdentical(
Expand Down