Pass in commissioning delegate in init parameters (#14510)

* Swappable commissioning state machine.

* Restyled by autopep8

* Change default on constructor.

This gets overwritten by init anyway, so it can be null.

* Fix typo.

* Restyled by autopep8

* Update src/controller/AutoCommissioner.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Restyled by autopep8

Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/scripts/tests/cirque_tests.sh b/scripts/tests/cirque_tests.sh
index ff6996c..2a1062a 100755
--- a/scripts/tests/cirque_tests.sh
+++ b/scripts/tests/cirque_tests.sh
@@ -38,6 +38,7 @@
     "EchoTest"
     "EchoOverTcpTest"
     "MobileDeviceTest"
+    "CommissioningTest"
     "InteractionModelTest"
 )
 
diff --git a/src/controller/AutoCommissioner.cpp b/src/controller/AutoCommissioner.cpp
index 913dc97..d14db40 100644
--- a/src/controller/AutoCommissioner.cpp
+++ b/src/controller/AutoCommissioner.cpp
@@ -221,15 +221,21 @@
     return CommissioningStage::kError;
 }
 
-void AutoCommissioner::StartCommissioning(CommissioneeDeviceProxy * proxy)
+CHIP_ERROR AutoCommissioner::StartCommissioning(DeviceCommissioner * commissioner, CommissioneeDeviceProxy * proxy)
 {
     // TODO: check that there is no commissioning in progress currently.
+    if (commissioner == nullptr)
+    {
+        ChipLogError(Controller, "Invalid DeviceCommissioner");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
 
     if (proxy == nullptr || !proxy->GetSecureSession().HasValue())
     {
         ChipLogError(Controller, "Device proxy secure session error");
-        return;
+        return CHIP_ERROR_INVALID_ARGUMENT;
     }
+    mCommissioner            = commissioner;
     mCommissioneeDeviceProxy = proxy;
     mNeedsNetworkSetup =
         mCommissioneeDeviceProxy->GetSecureSession().Value()->AsSecureSession()->GetPeerAddress().GetTransportType() ==
@@ -237,6 +243,7 @@
     CHIP_ERROR err               = CHIP_NO_ERROR;
     CommissioningStage nextStage = GetNextCommissioningStage(CommissioningStage::kSecurePairing, err);
     mCommissioner->PerformCommissioningStep(mCommissioneeDeviceProxy, nextStage, mParams, this, 0, GetCommandTimeout(nextStage));
+    return CHIP_NO_ERROR;
 }
 
 Optional<System::Clock::Timeout> AutoCommissioner::GetCommandTimeout(CommissioningStage stage)
diff --git a/src/controller/AutoCommissioner.h b/src/controller/AutoCommissioner.h
index d836feb..5849ae6 100644
--- a/src/controller/AutoCommissioner.h
+++ b/src/controller/AutoCommissioner.h
@@ -28,15 +28,14 @@
 class AutoCommissioner : public CommissioningDelegate
 {
 public:
-    AutoCommissioner(DeviceCommissioner * commissioner) : mCommissioner(commissioner) {}
-    ~AutoCommissioner();
-    CHIP_ERROR SetCommissioningParameters(const CommissioningParameters & params);
-    void SetOperationalCredentialsDelegate(OperationalCredentialsDelegate * operationalCredentialsDelegate);
+    AutoCommissioner() {}
+    virtual ~AutoCommissioner();
+    CHIP_ERROR SetCommissioningParameters(const CommissioningParameters & params) override;
+    void SetOperationalCredentialsDelegate(OperationalCredentialsDelegate * operationalCredentialsDelegate) override;
 
-    void StartCommissioning(CommissioneeDeviceProxy * proxy);
+    virtual CHIP_ERROR StartCommissioning(DeviceCommissioner * commissioner, CommissioneeDeviceProxy * proxy) override;
 
-    // Delegate functions
-    CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err, CommissioningDelegate::CommissioningReport report) override;
+    virtual CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err, CommissioningDelegate::CommissioningReport report) override;
 
 private:
     CommissioningStage GetNextCommissioningStage(CommissioningStage currentStage, CHIP_ERROR & lastErr);
@@ -52,7 +51,7 @@
     CHIP_ERROR NOCChainGenerated(ByteSpan noc, ByteSpan icac, ByteSpan rcac, AesCcm128KeySpan ipk, NodeId adminSubject);
     Optional<System::Clock::Timeout> GetCommandTimeout(CommissioningStage stage);
 
-    DeviceCommissioner * mCommissioner;
+    DeviceCommissioner * mCommissioner                               = nullptr;
     CommissioneeDeviceProxy * mCommissioneeDeviceProxy               = nullptr;
     OperationalDeviceProxy * mOperationalDeviceProxy                 = nullptr;
     OperationalCredentialsDelegate * mOperationalCredentialsDelegate = nullptr;
diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp
index f4feb64..814d7a9 100644
--- a/src/controller/CHIPDeviceController.cpp
+++ b/src/controller/CHIPDeviceController.cpp
@@ -644,7 +644,7 @@
 DeviceCommissioner::DeviceCommissioner() :
     mOnDeviceConnectedCallback(OnDeviceConnectedFn, this), mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureFn, this),
     mDeviceAttestationInformationVerificationCallback(OnDeviceAttestationInformationVerification, this),
-    mDeviceNOCChainCallback(OnDeviceNOCChainGeneration, this), mSetUpCodePairer(this), mAutoCommissioner(this)
+    mDeviceNOCChainCallback(OnDeviceNOCChainGeneration, this), mSetUpCodePairer(this)
 {
     mPairingDelegate         = nullptr;
     mPairedDevicesUpdated    = false;
@@ -674,6 +674,14 @@
 #endif
 
     mPairingDelegate = params.pairingDelegate;
+    if (params.defaultCommissioner != nullptr)
+    {
+        mDefaultCommissioner = params.defaultCommissioner;
+    }
+    else
+    {
+        mDefaultCommissioner = &mAutoCommissioner;
+    }
 
 #if CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY // make this commissioner discoverable
     mUdcTransportMgr = chip::Platform::New<DeviceIPTransportMgr>();
@@ -942,11 +950,11 @@
     mSystemState->SystemLayer()->StartTimer(chip::System::Clock::Milliseconds32(kSessionEstablishmentTimeout),
                                             OnSessionEstablishmentTimeoutCallback, this);
 
-    mAutoCommissioner.SetOperationalCredentialsDelegate(mOperationalCredentialsDelegate);
-    ReturnErrorOnFailure(mAutoCommissioner.SetCommissioningParameters(params));
+    mDefaultCommissioner->SetOperationalCredentialsDelegate(mOperationalCredentialsDelegate);
+    ReturnErrorOnFailure(mDefaultCommissioner->SetCommissioningParameters(params));
     if (device->IsSecureConnected())
     {
-        mAutoCommissioner.StartCommissioning(device);
+        mDefaultCommissioner->StartCommissioning(this, device);
     }
     else
     {
@@ -1036,7 +1044,7 @@
     if (mRunCommissioningAfterConnection)
     {
         mRunCommissioningAfterConnection = false;
-        mAutoCommissioner.StartCommissioning(mDeviceBeingCommissioned);
+        mDefaultCommissioner->StartCommissioning(this, mDeviceBeingCommissioned);
     }
     else
     {
diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h
index 2978382..155914c 100644
--- a/src/controller/CHIPDeviceController.h
+++ b/src/controller/CHIPDeviceController.h
@@ -153,7 +153,8 @@
 
 struct CommissionerInitParams : public ControllerInitParams
 {
-    DevicePairingDelegate * pairingDelegate = nullptr;
+    DevicePairingDelegate * pairingDelegate     = nullptr;
+    CommissioningDelegate * defaultCommissioner = nullptr;
 };
 
 typedef void (*OnOpenCommissioningWindow)(void * context, NodeId deviceId, CHIP_ERROR status, SetupPayload payload);
@@ -858,7 +859,10 @@
     Callback::Callback<OnNOCChainGeneration> mDeviceNOCChainCallback;
     SetUpCodePairer mSetUpCodePairer;
     AutoCommissioner mAutoCommissioner;
-    CommissioningDelegate * mCommissioningDelegate = nullptr;
+    CommissioningDelegate * mDefaultCommissioner =
+        nullptr; // Commissioning delegate to call when PairDevice / Commission functions are used
+    CommissioningDelegate * mCommissioningDelegate =
+        nullptr; // Commissioning delegate that issued the PerformCommissioningStep command
 };
 
 } // namespace Controller
diff --git a/src/controller/CHIPDeviceControllerFactory.cpp b/src/controller/CHIPDeviceControllerFactory.cpp
index 7e1759f..a631ebd 100644
--- a/src/controller/CHIPDeviceControllerFactory.cpp
+++ b/src/controller/CHIPDeviceControllerFactory.cpp
@@ -191,7 +191,8 @@
 
     CommissionerInitParams commissionerParams;
     PopulateInitParams(commissionerParams, params);
-    commissionerParams.pairingDelegate = params.pairingDelegate;
+    commissionerParams.pairingDelegate     = params.pairingDelegate;
+    commissionerParams.defaultCommissioner = params.defaultCommissioner;
 
     CHIP_ERROR err = commissioner.Init(commissionerParams);
     return err;
diff --git a/src/controller/CHIPDeviceControllerFactory.h b/src/controller/CHIPDeviceControllerFactory.h
index ca2667c..eb9b668 100644
--- a/src/controller/CHIPDeviceControllerFactory.h
+++ b/src/controller/CHIPDeviceControllerFactory.h
@@ -61,6 +61,7 @@
     DevicePairingDelegate * pairingDelegate = nullptr;
 
     Credentials::DeviceAttestationVerifier * deviceAttestationVerifier = nullptr;
+    CommissioningDelegate * defaultCommissioner                        = nullptr;
 };
 
 // TODO everything other than the fabric storage here should be removed.
diff --git a/src/controller/CommissioningDelegate.h b/src/controller/CommissioningDelegate.h
index d52eef5..2425b37 100644
--- a/src/controller/CommissioningDelegate.h
+++ b/src/controller/CommissioningDelegate.h
@@ -24,6 +24,8 @@
 namespace chip {
 namespace Controller {
 
+class DeviceCommissioner;
+
 enum CommissioningStage : uint8_t
 {
     kError,
@@ -250,7 +252,10 @@
         CommissioningStage stageCompleted;
         // TODO: Add other things the delegate needs to know.
     };
-    virtual CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err, CommissioningReport report) = 0;
+    virtual CHIP_ERROR SetCommissioningParameters(const CommissioningParameters & params)                           = 0;
+    virtual void SetOperationalCredentialsDelegate(OperationalCredentialsDelegate * operationalCredentialsDelegate) = 0;
+    virtual CHIP_ERROR StartCommissioning(DeviceCommissioner * commissioner, CommissioneeDeviceProxy * proxy)       = 0;
+    virtual CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err, CommissioningReport report)                        = 0;
 };
 
 } // namespace Controller
diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
index 6c687cb..58100b4 100644
--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp
+++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
@@ -50,6 +50,7 @@
 #include <app/DeviceProxy.h>
 #include <app/InteractionModelEngine.h>
 #include <app/server/Dnssd.h>
+#include <controller/AutoCommissioner.h>
 #include <controller/CHIPDeviceController.h>
 #include <controller/CHIPDeviceControllerFactory.h>
 #include <controller/CommissioningDelegate.h>
@@ -88,6 +89,7 @@
 chip::Platform::ScopedMemoryBuffer<uint8_t> sCredsBuf;
 chip::Platform::ScopedMemoryBuffer<uint8_t> sThreadBuf;
 chip::Controller::CommissioningParameters sCommissioningParameters;
+
 } // namespace
 
 chip::Controller::ScriptDevicePairingDelegate sPairingDelegate;
@@ -104,7 +106,7 @@
 ChipError::StorageType pychip_DeviceController_StackInit();
 
 ChipError::StorageType pychip_DeviceController_NewDeviceController(chip::Controller::DeviceCommissioner ** outDevCtrl,
-                                                                   chip::NodeId localDeviceId);
+                                                                   chip::NodeId localDeviceId, bool useTestCommissioner);
 ChipError::StorageType pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl);
 ChipError::StorageType pychip_DeviceController_GetAddressAndPort(chip::Controller::DeviceCommissioner * devCtrl,
                                                                  chip::NodeId nodeId, char * outAddress, uint64_t maxAddressLen,
diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp
index 4f01d64..a943673 100644
--- a/src/controller/python/OpCredsBinding.cpp
+++ b/src/controller/python/OpCredsBinding.cpp
@@ -89,6 +89,20 @@
 extern chip::Controller::Python::StorageAdapter * sStorageAdapter;
 extern chip::Controller::ScriptDeviceAddressUpdateDelegate sDeviceAddressUpdateDelegate;
 extern chip::Controller::ScriptDevicePairingDelegate sPairingDelegate;
+bool sTestCommissionerUsed = false;
+class TestCommissioner : public chip::Controller::AutoCommissioner
+{
+public:
+    TestCommissioner() : AutoCommissioner() {}
+    ~TestCommissioner() {}
+    CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err,
+                                         chip::Controller::CommissioningDelegate::CommissioningReport report) override
+    {
+        sTestCommissionerUsed = true;
+        return chip::Controller::AutoCommissioner::CommissioningStepFinished(err, report);
+    }
+};
+TestCommissioner sTestCommissioner;
 
 extern "C" {
 struct OpCredsContext
@@ -117,7 +131,7 @@
 
 ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * context,
                                                          chip::Controller::DeviceCommissioner ** outDevCtrl, uint8_t fabricIndex,
-                                                         FabricId fabricId, chip::NodeId nodeId)
+                                                         FabricId fabricId, chip::NodeId nodeId, bool useTestCommissioner)
 {
     ChipLogDetail(Controller, "Creating New Device Controller");
 
@@ -159,6 +173,10 @@
     initParams.controllerRCAC                 = rcacSpan;
     initParams.controllerICAC                 = icacSpan;
     initParams.controllerNOC                  = nocSpan;
+    if (useTestCommissioner)
+    {
+        initParams.defaultCommissioner = &sTestCommissioner;
+    }
 
     err = Controller::DeviceControllerFactory::GetInstance().SetupCommissioner(initParams, *devCtrl);
     VerifyOrReturnError(err == CHIP_NO_ERROR, err.AsInteger());
@@ -183,4 +201,10 @@
 
     return CHIP_NO_ERROR.AsInteger();
 }
+
+bool pychip_TestCommissionerUsed()
+{
+    return sTestCommissionerUsed;
 }
+
+} // extern "C"
diff --git a/src/controller/python/chip-device-ctrl.py b/src/controller/python/chip-device-ctrl.py
index 8f4aa91..cfd79eb 100755
--- a/src/controller/python/chip-device-ctrl.py
+++ b/src/controller/python/chip-device-ctrl.py
@@ -179,9 +179,10 @@
 
         self.bleMgr = None
 
-        self.chipStack = ChipStack.ChipStack(bluetoothAdapter=bluetoothAdapter)
+        self.chipStack = ChipStack.ChipStack(
+            bluetoothAdapter=bluetoothAdapter, persistentStoragePath='/tmp/chip-device-ctrl-storage.json')
         self.fabricAdmin = FabricAdmin.FabricAdmin()
-        self.devCtrl = self.fabricAdmin.NewController(controllerNodeId)
+        self.devCtrl = self.fabricAdmin.NewController(controllerNodeId, True)
 
         self.commissionableNodeCtrl = ChipCommissionableNodeCtrl.ChipCommissionableNodeController(
             self.chipStack)
diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py
index 3390d01..8361a28 100644
--- a/src/controller/python/chip/ChipDeviceCtrl.py
+++ b/src/controller/python/chip/ChipDeviceCtrl.py
@@ -86,7 +86,7 @@
 class ChipDeviceController():
     activeList = set()
 
-    def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, fabricIndex: int, nodeId: int):
+    def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, fabricIndex: int, nodeId: int, useTestCommissioner: bool = False):
         self.state = DCState.NOT_INITIALIZED
         self.devCtrl = None
         self._ChipStack = builtins.chipStack
@@ -98,7 +98,7 @@
 
         res = self._ChipStack.Call(
             lambda: self._dmLib.pychip_OpCreds_AllocateController(ctypes.c_void_p(
-                opCredsContext), pointer(devCtrl), fabricIndex, fabricId, nodeId)
+                opCredsContext), pointer(devCtrl), fabricIndex, fabricId, nodeId, useTestCommissioner)
         )
 
         if res != 0:
@@ -280,6 +280,11 @@
             return False
         return self._ChipStack.commissioningEventRes == 0
 
+    def GetTestCommissionerUsed(self):
+        return self._ChipStack.Call(
+            lambda: self._dmLib.pychip_TestCommissionerUsed()
+        )
+
     def CommissionIP(self, ipaddr, setupPinCode, nodeid):
         self.CheckIsActive()
 
@@ -918,3 +923,5 @@
             self._dmLib.pychip_DeviceController_OpenCommissioningWindow.argtypes = [
                 c_void_p, c_uint64, c_uint16, c_uint16, c_uint16, c_uint8]
             self._dmLib.pychip_DeviceController_OpenCommissioningWindow.restype = c_uint32
+            self._dmLib.pychip_TestCommissionerUsed.argtypes = []
+            self._dmLib.pychip_TestCommissionerUsed.restype = c_bool
diff --git a/src/controller/python/chip/FabricAdmin.py b/src/controller/python/chip/FabricAdmin.py
index 362c7da..63e6c89 100644
--- a/src/controller/python/chip/FabricAdmin.py
+++ b/src/controller/python/chip/FabricAdmin.py
@@ -152,7 +152,7 @@
 
         FabricAdmin.activeAdmins.add(self)
 
-    def NewController(self, nodeId: int = None):
+    def NewController(self, nodeId: int = None, useTestCommissioner: bool = False):
         ''' Vend a new controller on this fabric seeded with the right fabric details.
         '''
         if (not(self._isActive)):
@@ -166,7 +166,7 @@
         print(
             f"Allocating new controller with FabricId: {self._fabricId}({self._fabricIndex}), NodeId: {nodeId}")
         controller = ChipDeviceCtrl.ChipDeviceController(
-            self.closure, self._fabricId, self._fabricIndex, nodeId)
+            self.closure, self._fabricId, self._fabricIndex, nodeId, useTestCommissioner)
         return controller
 
     def ShutdownAll():
diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py
index 593d28c..958e296 100644
--- a/src/controller/python/test/test_scripts/base.py
+++ b/src/controller/python/test/test_scripts/base.py
@@ -103,11 +103,11 @@
 
 
 class BaseTestHelper:
-    def __init__(self, nodeid: int):
+    def __init__(self, nodeid: int, testCommissioner: bool = False):
         self.chipStack = ChipStack('/tmp/repl_storage.json')
         self.fabricAdmin = chip.FabricAdmin.FabricAdmin(
             fabricId=1, fabricIndex=1)
-        self.devCtrl = self.fabricAdmin.NewController(nodeid)
+        self.devCtrl = self.fabricAdmin.NewController(nodeid, testCommissioner)
         self.controllerNodeId = nodeid
         self.logger = logger
 
@@ -144,6 +144,9 @@
         self.logger.info("Device finished key exchange.")
         return True
 
+    def TestUsedTestCommissioner(self):
+        return self.devCtrl.GetTestCommissionerUsed()
+
     async def TestMultiFabric(self, ip: str, setuppin: int, nodeid: int):
         self.logger.info("Opening Commissioning Window")
 
diff --git a/src/controller/python/test/test_scripts/commissioning_test.py b/src/controller/python/test/test_scripts/commissioning_test.py
new file mode 100755
index 0000000..198de12
--- /dev/null
+++ b/src/controller/python/test/test_scripts/commissioning_test.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+
+#
+#    Copyright (c) 2021 Project CHIP Authors
+#    All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+# Commissioning test.
+import os
+import sys
+from optparse import OptionParser
+from base import TestFail, TestTimeout, BaseTestHelper, FailIfNot, logger
+from cluster_objects import NODE_ID, ClusterObjectTests
+from network_commissioning import NetworkCommissioningTests
+import asyncio
+
+# The thread network dataset tlv for testing, splited into T-L-V.
+
+TEST_THREAD_NETWORK_DATASET_TLV = "0e080000000000010000" + \
+    "000300000c" + \
+    "35060004001fffe0" + \
+    "0208fedcba9876543210" + \
+    "0708fd00000000001234" + \
+    "0510ffeeddccbbaa99887766554433221100" + \
+    "030e54657374696e674e6574776f726b" + \
+    "0102d252" + \
+    "041081cb3b2efa781cc778397497ff520fa50c0302a0ff"
+# Network id, for the thread network, current a const value, will be changed to XPANID of the thread network.
+TEST_THREAD_NETWORK_ID = "fedcba9876543210"
+TEST_DISCRIMINATOR = 3840
+
+ENDPOINT_ID = 0
+LIGHTING_ENDPOINT_ID = 1
+GROUP_ID = 0
+
+
+def main():
+    optParser = OptionParser()
+    optParser.add_option(
+        "-t",
+        "--timeout",
+        action="store",
+        dest="testTimeout",
+        default=75,
+        type='int',
+        help="The program will return with timeout after specified seconds.",
+        metavar="<timeout-second>",
+    )
+    optParser.add_option(
+        "-a",
+        "--address",
+        action="store",
+        dest="deviceAddress",
+        default='',
+        type='str',
+        help="Address of the device",
+        metavar="<device-addr>",
+    )
+
+    (options, remainingArgs) = optParser.parse_args(sys.argv[1:])
+
+    timeoutTicker = TestTimeout(options.testTimeout)
+    timeoutTicker.start()
+
+    test = BaseTestHelper(nodeid=112233, testCommissioner=True)
+
+    logger.info("Testing discovery")
+    FailIfNot(test.TestDiscovery(discriminator=TEST_DISCRIMINATOR),
+              "Failed to discover any devices.")
+
+    FailIfNot(test.SetNetworkCommissioningParameters(dataset=TEST_THREAD_NETWORK_DATASET_TLV),
+              "Failed to finish network commissioning")
+
+    logger.info("Testing key exchange")
+    FailIfNot(test.TestKeyExchange(ip=options.deviceAddress,
+                                   setuppin=20202021,
+                                   nodeid=1),
+              "Failed to finish key exchange")
+
+    logger.info("Testing on off cluster")
+    FailIfNot(test.TestOnOffCluster(nodeid=1,
+                                    endpoint=LIGHTING_ENDPOINT_ID,
+                                    group=GROUP_ID), "Failed to test on off cluster")
+
+    FailIfNot(test.TestUsedTestCommissioner(),
+              "Test commissioner check failed")
+
+    timeoutTicker.stop()
+
+    logger.info("Test finished")
+
+    # TODO: Python device controller cannot be shutdown clean sometimes and will block on AsyncDNSResolverSockets shutdown.
+    # Call os._exit(0) to force close it.
+    os._exit(0)
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except Exception as ex:
+        logger.exception(ex)
+        TestFail("Exception occurred when running tests.")
diff --git a/src/test_driver/linux-cirque/CommissioningTest.py b/src/test_driver/linux-cirque/CommissioningTest.py
new file mode 100755
index 0000000..e9e311d
--- /dev/null
+++ b/src/test_driver/linux-cirque/CommissioningTest.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""
+Copyright (c) 2021 Project CHIP Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+import logging
+import os
+import pprint
+import time
+import sys
+
+from helper.CHIPTestBase import CHIPVirtualHome
+
+logger = logging.getLogger('MobileDeviceTest')
+logger.setLevel(logging.INFO)
+
+sh = logging.StreamHandler()
+sh.setFormatter(
+    logging.Formatter(
+        '%(asctime)s [%(name)s] %(levelname)s %(message)s'))
+logger.addHandler(sh)
+
+CHIP_PORT = 5540
+
+CIRQUE_URL = "http://localhost:5000"
+CHIP_REPO = os.path.join(os.path.abspath(
+    os.path.dirname(__file__)), "..", "..", "..")
+TEST_EXTPANID = "fedcba9876543210"
+
+DEVICE_CONFIG = {
+    'device0': {
+        'type': 'MobileDevice',
+        'base_image': 'connectedhomeip/chip-cirque-device-base',
+        'capability': ['TrafficControl', 'Mount'],
+        'rcp_mode': True,
+        'docker_network': 'Ipv6',
+        'traffic_control': {'latencyMs': 100},
+        "mount_pairs": [[CHIP_REPO, CHIP_REPO]],
+    },
+    'device1': {
+        'type': 'CHIPEndDevice',
+        'base_image': 'connectedhomeip/chip-cirque-device-base',
+        'capability': ['Thread', 'TrafficControl', 'Mount'],
+        'rcp_mode': True,
+        'docker_network': 'Ipv6',
+        'traffic_control': {'latencyMs': 100},
+        "mount_pairs": [[CHIP_REPO, CHIP_REPO]],
+    }
+}
+
+
+class TestCommissioner(CHIPVirtualHome):
+    def __init__(self, device_config):
+        super().__init__(CIRQUE_URL, device_config)
+        self.logger = logger
+
+    def setup(self):
+        self.initialize_home()
+
+    def test_routine(self):
+        self.run_controller_test()
+
+    def run_controller_test(self):
+        ethernet_ip = [device['description']['ipv6_addr'] for device in self.non_ap_devices
+                       if device['type'] == 'CHIPEndDevice'][0]
+        server_ids = [device['id'] for device in self.non_ap_devices
+                      if device['type'] == 'CHIPEndDevice']
+        req_ids = [device['id'] for device in self.non_ap_devices
+                   if device['type'] == 'MobileDevice']
+
+        for server in server_ids:
+            self.execute_device_cmd(server, "CHIPCirqueDaemon.py -- run gdb -return-child-result -q -ex \"set pagination off\" -ex run -ex \"bt 25\" --args {} --thread".format(
+                os.path.join(CHIP_REPO, "out/debug/standalone/chip-all-clusters-app")))
+
+        self.reset_thread_devices(server_ids)
+
+        req_device_id = req_ids[0]
+
+        self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join(
+            CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip-0.0-cp37-abi3-linux_x86_64.whl")))
+
+        command = "gdb -return-child-result -q -ex run -ex bt --args python3 {} -t 150 -a {}".format(
+            os.path.join(
+                CHIP_REPO, "src/controller/python/test/test_scripts/commissioning_test.py"),
+            ethernet_ip)
+        ret = self.execute_device_cmd(req_device_id, command)
+
+        self.assertEqual(ret['return_code'], '0',
+                         "Test failed: non-zero return code")
+
+
+if __name__ == "__main__":
+    sys.exit(TestCommissioner(DEVICE_CONFIG).run_test())