-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add some Swift tests for MTRDevice. (#29540)
- Loading branch information
1 parent
9fb8e57
commit 8717288
Showing
3 changed files
with
412 additions
and
9 deletions.
There are no files selected for viewing
397 changes: 397 additions & 0 deletions
397
src/darwin/Framework/CHIPTests/MTRSwiftDeviceTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.