Skip to content

Commit

Permalink
Add some Swift tests for MTRDevice.
Browse files Browse the repository at this point in the history
  • Loading branch information
bzbarsky-apple committed Oct 3, 2023
1 parent 1ec98c8 commit 94028ce
Show file tree
Hide file tree
Showing 3 changed files with 412 additions and 9 deletions.
397 changes: 397 additions & 0 deletions src/darwin/Framework/CHIPTests/MTRSwiftDeviceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,397 @@
import Matter
import XCTest

// This should eventually grow into a Swift copy of MTRDeviceTests

struct DeviceConstants {
static let testVendorID = 0xFFF1
static let onboardingPayload = "MT:-24J0AFN00KA0648G00"
static let deviceID = 0x12344321
static let timeoutInSeconds : UInt16 = 3
static let pairingTimeoutInSeconds : UInt16 = 10
}

var sConnectedDevice: MTRBaseDevice? = nil

var sController: MTRDeviceController? = nil

var sTestKeys: MTRTestKeys? = nil

// Because we are using things from Matter.framework that are flagged
// as only being available starting with macOS 13.3, we need to flag our
// code with the same availabiluty annotation.
@available(macOS, introduced: 13.3)
@available(iOS, introduced: 16.4)
class MTRSwiftDeviceTestControllerDelegate : NSObject, MTRDeviceControllerDelegate {
let expectation: XCTestExpectation

init(withExpectation providedExpectation: XCTestExpectation) {
expectation = providedExpectation
}

func controller(_ controller: MTRDeviceController, statusUpdate status: MTRCommissioningStatus) {
XCTAssertNotEqual(status, MTRCommissioningStatus.failed)
}

func controller(_ controller: MTRDeviceController, commissioningSessionEstablishmentDone error: Error?) {
XCTAssertNil(error)

do {
try controller.commissionNode(withID: DeviceConstants.deviceID as NSNumber, commissioningParams: MTRCommissioningParameters())
} catch {
XCTFail("Could not start commissioning of node: \(error)")
}

// Keep waiting for commissioningComplete
}

func controller(_ controller: MTRDeviceController, commissioningComplete error: Error?, nodeID: NSNumber?) {
XCTAssertNil(error)
XCTAssertEqual(nodeID, DeviceConstants.deviceID as NSNumber)
sConnectedDevice = MTRBaseDevice(nodeID: nodeID!, controller: controller)
expectation.fulfill()
}
}

typealias MTRDeviceTestDelegateDataHandler = ([[ String: Any ]]) -> Void

class MTRSwiftDeviceTestDelegate : NSObject, MTRDeviceDelegate {
var onReachable : () -> Void
var onNotReachable : (() -> Void)? = nil
var onAttributeDataReceived : MTRDeviceTestDelegateDataHandler? = nil
var onEventDataReceived : MTRDeviceTestDelegateDataHandler? = nil
var onReportEnd : (() -> Void)? = nil

init(withReachableHandler handler : @escaping () -> Void) {
onReachable = handler
}

func device(_ device: MTRDevice, stateChanged state : MTRDeviceState) {
if (state == MTRDeviceState.reachable) {
onReachable()
} else {
onNotReachable?()
}
}

func device(_ device : MTRDevice, receivedAttributeReport attributeReport : [[ String: Any ]])
{
onAttributeDataReceived?(attributeReport)
}

func device(_ device : MTRDevice, receivedEventReport eventReport : [[ String : Any ]])
{
onEventDataReceived?(eventReport)
}

@objc func unitTestReportEnd(forDevice : MTRDevice)
{
onReportEnd?()
}
}

// Because we are using things from Matter.framework that are flagged
// as only being available starting with macOS 13.5, we need to flag our
// code with the same availability annotation.
@available(macOS, introduced: 14.1)
@available(iOS, introduced: 17.1)
class MTRSwiftDeviceTests : XCTestCase {
static var sStackInitRan : Bool = false
static var sNeedsStackShutdown : Bool = true

static override func tearDown() {
// Global teardown, runs once
if (sNeedsStackShutdown) {
// We don't need to worry about ResetCommissionee. If we get here,
// we're running only one of our test methods (using
// -only-testing:MatterTests/MTRSwiftDeviceTests/testMethodName), since
// we did not run test999_TearDown.
shutdownStack()
}
}

override func setUp()
{
// Per-test setup, runs before each test.
super.setUp()
self.continueAfterFailure = false

if (!MTRSwiftDeviceTests.sStackInitRan) {
initStack()
}
}

override func tearDown()
{
// Per-test teardown, runs after each test.
super.tearDown()
}

func initStack()
{
MTRSwiftDeviceTests.sStackInitRan = true

let factory = MTRDeviceControllerFactory.sharedInstance()

let storage = MTRTestStorage()
let factoryParams = MTRDeviceControllerFactoryParams(storage: storage)

do {
try factory.start(factoryParams)
} catch {
XCTFail("Count not start controller factory: \(error)")
}
XCTAssertTrue(factory.isRunning)

let testKeys = MTRTestKeys()

sTestKeys = testKeys

// Needs to match what startControllerOnExistingFabric calls elsewhere in
// this file do.
let params = MTRDeviceControllerStartupParams(ipk: testKeys.ipk, fabricID: 1, nocSigner:testKeys)
params.vendorID = DeviceConstants.testVendorID as NSNumber

let controller : MTRDeviceController
do {
controller = try factory.createController(onNewFabric: params)
} catch {
XCTFail("Could not create controller: \(error)")
return
}
XCTAssertTrue(controller.isRunning)

sController = controller

let expectation = expectation(description : "Commissioning Complete")

let controllerDelegate = MTRSwiftDeviceTestControllerDelegate(withExpectation: expectation)
let serialQueue = DispatchQueue(label: "com.chip.device_controller_delegate")

controller.setDeviceControllerDelegate(controllerDelegate, queue: serialQueue)

let payload : MTRSetupPayload
do {
payload = try MTRSetupPayload(onboardingPayload: DeviceConstants.onboardingPayload)
} catch {
XCTFail("Could not parse setup payload: \(error)")
return
}

do {
try controller.setupCommissioningSession(with:payload, newNodeID: DeviceConstants.deviceID as NSNumber)
} catch {
XCTFail("Could not start setting up PASE session: \(error)")
return }

wait(for: [expectation], timeout: TimeInterval(DeviceConstants.pairingTimeoutInSeconds))
}

static func shutdownStack()
{
sNeedsStackShutdown = false

let controller = sController
XCTAssertNotNil(controller)

controller!.shutdown()
XCTAssertFalse(controller!.isRunning)

MTRDeviceControllerFactory.sharedInstance().stop()
}

func test000_SetUp()
{
// Nothing to do here; our setUp method handled this already. This test
// just exists to make the setup not look like it's happening inside other
// tests.
}

func test017_TestMTRDeviceBasics()
{
let device = MTRDevice(nodeID: DeviceConstants.deviceID as NSNumber, controller:sController!)
let queue = DispatchQueue.main

// Given reachable state becomes true before underlying OnSubscriptionEstablished callback, this expectation is necessary but
// not sufficient as a mark to the end of reports
let subscriptionExpectation = expectation(description: "Subscription has been set up")

let delegate = MTRSwiftDeviceTestDelegate(withReachableHandler: { () -> Void in
subscriptionExpectation.fulfill()
})

var attributeReportsReceived : Int = 0
delegate.onAttributeDataReceived = { (data: [[ String: Any ]]) -> Void in
attributeReportsReceived += data.count
}

// This is dependent on current implementation that priming reports send attributes and events in that order, and also that
// events in this test would fit in one report. So receiving events would mean all attributes and events have been received, and
// can satisfy the test below.
let gotReportsExpectation = expectation(description: "Attribute and Event reports have been received")
var eventReportsReceived : Int = 0
delegate.onEventDataReceived = { (eventReport: [[ String: Any ]]) -> Void in
eventReportsReceived += eventReport.count

for eventDict in eventReport {
let eventTimeTypeNumber = eventDict[MTREventTimeTypeKey] as! NSNumber?
XCTAssertNotNil(eventTimeTypeNumber)
let eventTimeType = MTREventTimeType(rawValue: eventTimeTypeNumber!.uintValue)
XCTAssert((eventTimeType == MTREventTimeType.systemUpTime) || (eventTimeType == MTREventTimeType.timestampDate))
if (eventTimeType == MTREventTimeType.systemUpTime) {
XCTAssertNotNil(eventDict[MTREventSystemUpTimeKey])
XCTAssertNotNil(device.estimatedStartTime)
} else if (eventTimeType == MTREventTimeType.timestampDate) {
XCTAssertNotNil(eventDict[MTREventTimestampDateKey])
}
}
}
delegate.onReportEnd = { () -> Void in
gotReportsExpectation.fulfill()
}

device.setDelegate(_: delegate, queue:queue)

// Test batching and duplicate check
// - Read 13 different attributes in a row, expect that the 1st to go out by itself, the next 9 batch, and then the 3 after
// are correctly queued in one batch
// - Then read 3 duplicates and expect them to be filtered
// - Note that these tests can only be verified via logs
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 0, params: nil)

device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 1, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 2, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 3, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 4, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 5, params: nil)

device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 6, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.scenesID.rawValue), attributeID: 7, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 0, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 1, params: nil)

device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 2, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 3, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 4, params: nil)

device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 4, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 4, params: nil)
device.readAttribute(withEndpointID: 1, clusterID: NSNumber(value: MTRClusterIDType.levelControlID.rawValue), attributeID: 4, params: nil)

wait(for: [ subscriptionExpectation, gotReportsExpectation ], timeout:60)

delegate.onReportEnd = nil

XCTAssertNotEqual(attributeReportsReceived, 0)
XCTAssertNotEqual(eventReportsReceived, 0)

// Before resubscribe, first test write failure and expected value effects
let testEndpointID = 1 as NSNumber
let testClusterID = 8 as NSNumber
let testAttributeID = 10000 as NSNumber // choose a nonexistent attribute to cause a failure
let expectedValueReportedExpectation = expectation(description: "Expected value reported")
let expectedValueRemovedExpectation = expectation(description: "Expected value removed")
delegate.onAttributeDataReceived = { (attributeReport: [[ String: Any ]]) -> Void in
for attributeDict in attributeReport {
let attributePath = attributeDict[MTRAttributePathKey] as! MTRAttributePath
XCTAssertNotNil(attributePath)
if (attributePath.endpoint == testEndpointID &&
attributePath.cluster == testClusterID &&
attributePath.attribute == testAttributeID) {
let data = attributeDict[MTRDataKey]
if (data != nil) {
expectedValueReportedExpectation.fulfill()
} else {
expectedValueRemovedExpectation.fulfill()
}
}
}
}

let writeValue = [ "type": "UnsignedInteger", "value": 200 ] as [String: Any]
device.writeAttribute(withEndpointID: testEndpointID,
clusterID: testClusterID,
attributeID: testAttributeID,
value: writeValue,
expectedValueInterval: 20000,
timedWriteTimeout:nil)

// expected value interval is 20s but expect it get reverted immediately as the write fails because it's writing to a
// nonexistent attribute
wait(for: [ expectedValueReportedExpectation, expectedValueRemovedExpectation ], timeout: 5, enforceOrder: true)

// Test if errors are properly received
let attributeReportErrorExpectation = expectation(description: "Attribute read error")
delegate.onAttributeDataReceived = { (data: [[ String: Any ]]) -> Void in
for attributeReponseValue in data {
if (attributeReponseValue[MTRErrorKey] != nil) {
attributeReportErrorExpectation.fulfill()
}
}
}
// use the nonexistent attribute and expect read error
device.readAttribute(withEndpointID: testEndpointID, clusterID: testClusterID, attributeID: testAttributeID, params: nil)
wait(for: [ attributeReportErrorExpectation ], timeout: 10)

// Resubscription test setup
let subscriptionDroppedExpectation = expectation(description: "Subscription has dropped")
delegate.onNotReachable = { () -> Void in
subscriptionDroppedExpectation.fulfill()
};
let resubscriptionExpectation = expectation(description: "Resubscription has happened")
delegate.onReachable = { () -> Void in
resubscriptionExpectation.fulfill()
};

// reset the onAttributeDataReceived to validate the following resubscribe test
attributeReportsReceived = 0;
eventReportsReceived = 0;
delegate.onAttributeDataReceived = { (data: [[ String: Any ]]) -> Void in
attributeReportsReceived += data.count;
};

delegate.onEventDataReceived = { (eventReport: [[ String: Any ]]) -> Void in
eventReportsReceived += eventReport.count;
};

// Now trigger another subscription which will cause ours to drop; we should re-subscribe after that.
let baseDevice = sConnectedDevice
let params = MTRSubscribeParams(minInterval: 1, maxInterval: 2)
params.shouldResubscribeAutomatically = false;
params.shouldReplaceExistingSubscriptions = true;
// Create second subscription which will cancel the first subscription. We
// can use a non-existent path here to cut down on the work that gets done.
baseDevice?.subscribeToAttributes(withEndpointID: 10000,
clusterID: 6,
attributeID: 0,
params: params,
queue: queue,
reportHandler: { (_: [[String : Any]]?, _: Error?) -> Void in
})

wait(for: [ subscriptionDroppedExpectation ], timeout:60)

// Check that device resets start time on subscription drop
XCTAssertNil(device.estimatedStartTime)

wait(for: [ resubscriptionExpectation ], timeout:60)

// Now make sure we ignore later tests. Ideally we would just unsubscribe
// or remove the delegate, but there's no good way to do that.
delegate.onReachable = { () -> Void in }
delegate.onNotReachable = nil
delegate.onAttributeDataReceived = nil
delegate.onEventDataReceived = nil

// Make sure we got no updated reports (because we had a cluster state cache
// with data versions) during the resubscribe.
XCTAssertEqual(attributeReportsReceived, 0);
XCTAssertEqual(eventReportsReceived, 0);
}

func test999_TearDown()
{
ResetCommissionee(sConnectedDevice, DispatchQueue.main, self, DeviceConstants.timeoutInSeconds)
type(of: self).shutdownStack()
}
}
Loading

0 comments on commit 94028ce

Please sign in to comment.