| 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); |
| } |
| |
| // Note: test027_AttestationChallenge is not implementable in Swift so far, |
| // because the attestationChallenge property is internal-only |
| |
| func test999_TearDown() |
| { |
| ResetCommissionee(sConnectedDevice, DispatchQueue.main, self, DeviceConstants.timeoutInSeconds) |
| type(of: self).shutdownStack() |
| } |
| } |