Add alarm API module ricxappframe.alarm.alarm 43/3943/7
authorLott, Christopher (cl778h) <cl778h@att.com>
Tue, 2 Jun 2020 00:53:12 +0000 (20:53 -0400)
committerLott, Christopher (cl778h) <cl778h@att.com>
Fri, 5 Jun 2020 15:07:17 +0000 (11:07 -0400)
New python module provides data types like AlarmDetail and
an AlarmManager class to create, raise and clear alarms by
sending messages via RMR, which should route to Alarm Adapter.
Builds JSON messages to match implementation in ric-plt/alarm-go.
Add JSON schema for alarm message as documentation.
Bump xappframepy version to 1.2.0.

Issue-ID: RIC-380
Signed-off-by: Lott, Christopher (cl778h) <cl778h@att.com>
Change-Id: I746bca0c0982314b6837c042b60747398a92f456

docs/alarm_api.rst [new file with mode: 0644]
docs/index.rst
docs/release-notes.rst
ricxappframe/alarm/__init__.py [new file with mode: 0644]
ricxappframe/alarm/alarm-schema.json [new file with mode: 0644]
ricxappframe/alarm/alarm.py [new file with mode: 0644]
tests/fixtures/test_local.rt
tests/test_alarm.py [new file with mode: 0644]
tox.ini

diff --git a/docs/alarm_api.rst b/docs/alarm_api.rst
new file mode 100644 (file)
index 0000000..0e7b262
--- /dev/null
@@ -0,0 +1,29 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2020 AT&T Intellectual Property
+
+RIC Alarm API
+=============
+
+Overview
+--------
+
+The xapp python framework package includes a python subpackage called
+`Alarm`.  This subpackage (`ricxappframe.alarm`) provides objects and
+methods for defining and raising alarms, which are transmitted using
+the RMR shared library.
+
+Usage of this python package requires that the RMR shared-object
+library is installed in a system library that is included in the
+directories found by default, usually something like /usr/local/lib.
+
+Alarm messages conform to the following JSON schema.
+
+.. literalinclude:: ../ricxappframe/alarm/alarm-schema.json
+  :language: JSON
+
+Alarm API
+---------
+
+.. automodule:: ricxappframe.alarm.alarm
+    :members:
index 5004b5b..56b3154 100644 (file)
@@ -14,6 +14,7 @@ xApp Python Framework
    overview.rst
    user-guide.rst
    rmr_api.rst
+   alarm_api.rst
    developer-guide.rst
    release-notes.rst
 
index b277b43..ff97a5e 100644 (file)
@@ -11,9 +11,10 @@ The format is based on `Keep a Changelog <http://keepachangelog.com/>`__
 and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 
-[1.2.0] - 2020-06-03
+[1.2.0] - 2020-06-04
 --------------------
 * Extend RMR module to support wormhole methods
+* Add alarm API (`RIC-380 <https://jira.o-ran-sc.org/browse/RIC-380>`_)
 
 
 [1.1.2] - 2020-05-13
@@ -46,7 +47,6 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [1.0.1] - 2020-04-10
 --------------------
-
 * Publish API documentation using Sphinx autodoc, which required
   changes so Sphinx can run when the RMR .so file is not available,
   such as during a ReadTheDocs build.
@@ -62,7 +62,6 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [1.0.0] - 4/6/2020
 ------------------
-
 * Python rmr has been moved into this repo. The module name has NOT
   changed in order to make the transition for repos very easy. The
   only transition needed should be prefixing rmr with ricxappframe in
@@ -71,7 +70,6 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [0.7.0] - 4/2/2020
 ------------------
-
 * RMRXapps by default now implement the rmr healthcheck probe;
   users can also override it with a more complex handler if they
   wish
@@ -81,13 +79,11 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [0.6.0] - 3/23/2020
 -------------------
-
 * Switch to SI95 for rmr
 
 
 [0.5.0] - 3/18/2020
 -------------------
-
 * All xapps (via the base class) now have a logger attribute that can
   be invoked to provide mdc logging. It is a passthrough to the RIC
   mdc logger for python (untouched, no value in an API on top at the
@@ -96,14 +92,12 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [0.4.1] - 3/17/2020
 -------------------
-
 * Switch tox to use py38
 * switch to latest builders
 
 
 [0.4.0] - 3/13/2020
 -------------------
-
 * Minor breaking change; switches the default behavior RE
   threading for RMRXapps. The default is not to return execution,
   but the caller (in `run`) can choose to loop in a thread.
@@ -112,7 +106,6 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [0.3.0] - 3/10/2020
 -------------------
-
 * Large change to the "feel" of this framework: rather than subclass
   instantiation, xapps now use initialization and registration
   functions to register handlers
@@ -127,7 +120,6 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [0.2.0] - 3/3/2020
 ------------------
-
 * now allows for RMRXapps to call code before entering the infinite
   loop
 * stop is now called before throwing NotImplemented in the case where
@@ -147,5 +139,4 @@ and this project adheres to `Semantic Versioning <http://semver.org/>`__.
 
 [0.1.0] - 2/27/2020
 -------------------
-
 * Initial commit
diff --git a/ricxappframe/alarm/__init__.py b/ricxappframe/alarm/__init__.py
new file mode 100644 (file)
index 0000000..82aac02
--- /dev/null
@@ -0,0 +1,16 @@
+# ==================================================================================
+#       Copyright (c) 2020 AT&T Intellectual Property.
+#       Copyright (c) 2020 Nokia
+#
+#   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.
+# ==================================================================================
diff --git a/ricxappframe/alarm/alarm-schema.json b/ricxappframe/alarm/alarm-schema.json
new file mode 100644 (file)
index 0000000..5e999ad
--- /dev/null
@@ -0,0 +1,94 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://gerrit.o-ran-sc.org/r/admin/repos/ric-plt/alarm-go.json",
+  "type": "object",
+  "title": "Alarm schema",
+  "description": "Schema for RIC alarm messages.",
+  "default": {},
+  "examples": [
+    {
+      "managedObjectId": "my-pod-lib",
+      "applicationId": "my-app",
+      "specificProblem": 1234,
+      "perceivedSeverity": "MAJOR",
+      "additionalInfo": "Some App data",
+      "identifyingInfo": "eth 0 1",
+      "AlarmAction": "RAISE",
+      "AlarmTime": 1591188407505707
+    }
+  ],
+  "required": [
+    "managedObjectId",
+    "applicationId",
+    "specificProblem",
+    "perceivedSeverity",
+    "identifyingInfo",
+    "AlarmAction",
+    "AlarmTime"
+  ],
+  "additionalProperties": true,
+  "properties": {
+    "managedObjectId": {
+      "type": "string",
+      "title": "The managedObjectId schema",
+      "description": "The name of the managed object that is the cause of the fault.",
+      "default": ""
+    },
+    "applicationId": {
+      "type": "string",
+      "title": "The applicationId schema",
+      "description": "The name of the process that raised the alarm.",
+      "default": ""
+    },
+    "specificProblem": {
+      "type": "integer",
+      "title": "The specificProblem schema",
+      "description": "The problem that is the cause of the alarm.",
+      "default": 0
+    },
+    "perceivedSeverity": {
+      "type": "string",
+      "enum": [
+        "UNSPECIFIED",
+        "CRITICAL",
+        "MAJOR",
+        "MINOR",
+        "WARNING",
+        "CLEARED",
+        "DEFAULT"
+      ],
+      "title": "The perceivedSeverity schema",
+      "description": "The severity of the alarm.",
+      "default": ""
+    },
+    "additionalInfo": {
+      "type": "string",
+      "title": "The additionalInfo schema",
+      "description": "Additional information given by the application (optional).",
+      "default": ""
+    },
+    "identifyingInfo": {
+      "type": "string",
+      "title": "The identifyingInfo schema",
+      "description": "Identifying additional information, which is part of alarm identity.",
+      "default": ""
+    },
+    "AlarmAction": {
+      "type": "string",
+      "enum": [
+        "RAISE",
+        "CLEAR",
+        "CLEARALL"
+      ],
+      "title": "The AlarmAction schema",
+      "description": "Action to perform on the alarm.",
+      "default": ""
+    },
+    "AlarmTime": {
+      "type": "integer",
+      "title": "The AlarmTime schema",
+      "description": "Current system time in milliseconds since the Epoch.",
+      "default": 0
+    }
+  }
+}
diff --git a/ricxappframe/alarm/alarm.py b/ricxappframe/alarm/alarm.py
new file mode 100644 (file)
index 0000000..9a3ba2d
--- /dev/null
@@ -0,0 +1,307 @@
+# ==================================================================================
+#       Copyright (c) 2020 AT&T Intellectual Property.
+#       Copyright (c) 2020 Nokia
+#
+#   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.
+# ==================================================================================
+"""
+Provides classes and methods to define, raise, reraise and clear alarms.
+All actions are implemented by sending RMR messages to the Alarm Adapter
+that comply with the JSON schema in file alarm-schema.json.
+"""
+
+from ctypes import c_void_p
+from enum import Enum, auto
+import json
+import time
+from mdclogpy import Logger
+from ricxappframe.rmr import rmr
+
+##############
+# PRIVATE API
+##############
+
+mdc_logger = Logger(name=__name__)
+RETRIES = 4
+
+##############
+# PUBLIC API
+##############
+
+# constants
+RIC_ALARM_UPDATE = 13111
+# RIC_ALARM_QUERY = 13112 # TBD
+
+# Publish dict keys as constants for convenience of client code.
+# Mixed lower/upper casing to comply with the Adapter JSON requirements.
+KEY_ALARM = "alarm"
+KEY_MANAGED_OBJECT_ID = "managedObjectId"
+KEY_APPLICATION_ID = "applicationId"
+KEY_SPECIFIC_PROBLEM = "specificProblem"
+KEY_PERCEIVED_SEVERITY = "perceivedSeverity"
+KEY_ADDITIONAL_INFO = "additionalInfo"
+KEY_IDENTIFYING_INFO = "identifyingInfo"
+KEY_ALARM_ACTION = "AlarmAction"
+KEY_ALARM_TIME = "AlarmTime"
+
+
+class AlarmAction(Enum):
+    """
+    Action to perform at the Alarm Adapter
+    """
+    RAISE = auto()
+    CLEAR = auto()
+    CLEARALL = auto()
+
+
+class AlarmSeverity(Enum):
+    """
+    Severity of an alarm
+    """
+    UNSPECIFIED = auto()
+    CRITICAL = auto()
+    MAJOR = auto()
+    MINOR = auto()
+    WARNING = auto()
+    CLEARED = auto()
+    DEFAULT = auto()
+
+
+class AlarmDetail(dict):
+    """
+    An alarm that can be raised or cleared.
+
+    Parameters
+    ----------
+    managed_object_id: str
+        The name of the managed object that is the cause of the fault (required)
+
+    application_id: str
+        The name of the process that raised the alarm (required)
+
+    specific_problem: int
+        The problem that is the cause of the alarm
+
+    perceived_severity: AlarmSeverity
+        The severity of the alarm, a value from the enum.
+
+    identifying_info: str
+        Identifying additional information, which is part of alarm identity
+
+    additional_info: str
+        Additional information given by the application (optional)
+    """
+    # pylint: disable=too-many-arguments
+    def __init__(self,
+                 managed_object_id: str,
+                 application_id: str,
+                 specific_problem: int,
+                 perceived_severity: AlarmSeverity,
+                 identifying_info: str,
+                 additional_info: str = ""):
+        """
+        Creates an object with the specified items.
+        """
+        dict.__init__(self)
+        self[KEY_MANAGED_OBJECT_ID] = managed_object_id
+        self[KEY_APPLICATION_ID] = application_id
+        self[KEY_SPECIFIC_PROBLEM] = specific_problem
+        self[KEY_PERCEIVED_SEVERITY] = perceived_severity.name
+        self[KEY_IDENTIFYING_INFO] = identifying_info
+        self[KEY_ADDITIONAL_INFO] = additional_info
+
+
+class AlarmManager:
+    """
+    Provides an API for an Xapp to raise and clear alarms by sending messages
+    via RMR, which should route the messages to an Alarm Adapter.
+
+    Parameters
+    ----------
+    vctx: ctypes c_void_p
+        Pointer to RMR context obtained by initializing RMR.
+        The context is used to allocate space and send messages.
+        The RMR routing table must have a destination for message
+        type RIC_ALARM_UPDATE as defined in this module.
+
+    managed_object_id: str
+        The name of the managed object that raises alarms
+
+    application_id: str
+        The name of the process that raises alarms
+    """
+    def __init__(self,
+                 vctx: c_void_p,
+                 managed_object_id: str,
+                 application_id: str):
+        """
+        Creates an alarm manager.
+        """
+        self.vctx = vctx
+        self.managed_object_id = managed_object_id
+        self.application_id = application_id
+
+    def create_alarm(self,
+                     specific_problem: int,
+                     perceived_severity: AlarmSeverity,
+                     identifying_info: str,
+                     additional_info: str = ""):
+        """
+        Convenience method that creates an alarm instance, an AlarmDetail object,
+        using cached values for managed object ID and application ID.
+
+        Parameters
+        ----------
+        specific_problem: int
+            The problem that is the cause of the alarm
+
+        perceived_severity: AlarmSeverity
+            The severity of the alarm, a value from the enum.
+
+        identifying_info: str
+            Identifying additional information, which is part of alarm identity
+
+        additional_info: str
+            Additional information given by the application (optional)
+
+        Returns
+        -------
+        AlarmDetail
+        """
+        return AlarmDetail(managed_object_id=self.managed_object_id,
+                           application_id=self.application_id,
+                           specific_problem=specific_problem, perceived_severity=perceived_severity,
+                           identifying_info=identifying_info, additional_info=additional_info)
+
+    @staticmethod
+    def _create_alarm_message(alarm: AlarmDetail, action: AlarmAction):
+        """
+        Creates a dict with the specified alarm detail plus action and time.
+        Uses the current system time in milliseconds since the Epoch.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            The alarm details.
+
+        action: AlarmAction
+            The action to perform at the Alarm Adapter on this alarm.
+        """
+        return {
+            **alarm,
+            KEY_ALARM_ACTION: action.name,
+            KEY_ALARM_TIME: int(round(time.time() * 1000))
+        }
+
+    def _rmr_send_alarm(self, msg: dict):
+        """
+        Serializes the dict and sends the result via RMR using a predefined message type.
+
+        Parameters
+        ----------
+        msg: dict
+            Dictionary with alarm message to encode and send
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        payload = json.dumps(msg).encode()
+        mdc_logger.debug("_rmr_send_alarm: payload is {}".format(payload))
+        sbuf = rmr.rmr_alloc_msg(vctx=self.vctx, size=len(payload), payload=payload,
+                                 mtype=RIC_ALARM_UPDATE, gen_transaction_id=True)
+
+        for _ in range(0, RETRIES):
+            sbuf = rmr.rmr_send_msg(self.vctx, sbuf)
+            post_send_summary = rmr.message_summary(sbuf)
+            mdc_logger.debug("_rmr_send_alarm: try {0} result is {1}".format(_, post_send_summary[rmr.RMR_MS_MSG_STATE]))
+            # stop trying if RMR does not indicate retry
+            if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_ERR_RETRY:
+                break
+
+        rmr.rmr_free_msg(sbuf)
+        if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_OK:
+            mdc_logger.warning("_rmr_send_alarm: failed after {} retries".format(RETRIES))
+            return False
+
+        return True
+
+    def raise_alarm(self, detail: AlarmDetail):
+        """
+        Builds and sends a message to the AlarmAdapter to raise an alarm
+        with the specified detail.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            Alarm to raise
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        msg = self._create_alarm_message(detail, AlarmAction.RAISE)
+        return self._rmr_send_alarm(msg)
+
+    def clear_alarm(self, detail: AlarmDetail):
+        """
+        Builds and sends a message to the AlarmAdapter to clear the alarm
+        with the specified detail.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            Alarm to clear
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        msg = self._create_alarm_message(detail, AlarmAction.CLEAR)
+        return self._rmr_send_alarm(msg)
+
+    def reraise_alarm(self, detail: AlarmDetail):
+        """
+        Builds and sends a message to the AlarmAdapter to clear the alarm with the
+        the specified detail, then builds and sends a message to raise the alarm again.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            Alarm to clear and raise again.
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        success = self.clear_alarm(detail)
+        if success:
+            success = self.raise_alarm(detail)
+        return success
+
+    def clear_all_alarms(self):
+        """
+        Builds and sends a message to the AlarmAdapter to clear all alarms.
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        detail = self.create_alarm(0, AlarmSeverity.DEFAULT, "", "")
+        msg = self._create_alarm_message(detail, AlarmAction.CLEARALL)
+        return self._rmr_send_alarm(msg)
index fd57f2c..10d9321 100644 (file)
@@ -1,6 +1,7 @@
 # do NOT use localhost, seems unresolved on jenkins VMs
-# first 3 lines are used for xapp frame tests
-# last four lines are used in the rmr submodule
+# first 3 lines (port 4564) are used for xapp frame tests
+# next 4 lines (port 3563-3564) are used in the rmr submodule
+# last line (port 4567) is used for alarms
 newrt|start
 mse| 60000 | -1 | 127.0.0.1:4564
 mse| 60001 | -1 | 127.0.0.1:4564
@@ -9,4 +10,5 @@ mse| 0 | -1 | 127.0.0.1:3563
 mse| 46656 | 777 | 127.0.0.1:3563
 mse| 1 | -1 | 127.0.0.1:3564
 mse| 2 | -1 | 127.0.0.1:3564
+mse| 13111 | -1 | 127.0.0.1:4567
 newrt|end
diff --git a/tests/test_alarm.py b/tests/test_alarm.py
new file mode 100644 (file)
index 0000000..0fad272
--- /dev/null
@@ -0,0 +1,114 @@
+# =================================================================================2
+#       Copyright (c) 2020 AT&T Intellectual Property.
+#       Copyright (c) 2020 Nokia
+#
+#   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 json
+import time
+from ricxappframe.alarm import alarm
+from ricxappframe.alarm.alarm import AlarmAction, AlarmDetail, AlarmManager, AlarmSeverity
+from ricxappframe.rmr import rmr
+
+MRC_SEND = None
+MRC_RCV = None
+SIZE = 256
+
+
+def setup_module():
+    """
+    test alarm module setup
+    """
+    global MRC_SEND
+    MRC_SEND = rmr.rmr_init(b"4566", rmr.RMR_MAX_RCV_BYTES, 0x00)
+    while rmr.rmr_ready(MRC_SEND) == 0:
+        time.sleep(1)
+
+    global MRC_RCV
+    MRC_RCV = rmr.rmr_init(b"4567", rmr.RMR_MAX_RCV_BYTES, 0x00)
+    while rmr.rmr_ready(MRC_RCV) == 0:
+        time.sleep(1)
+
+
+def teardown_module():
+    """
+    test alarm module teardown
+    """
+    rmr.rmr_close(MRC_SEND)
+
+
+def test_alarm_set_get():
+    """
+    test set functions
+    """
+    act = AlarmAction.RAISE
+    assert act is not None
+
+    sev = AlarmSeverity.CRITICAL
+    assert sev is not None
+
+    det = AlarmDetail("1", "2", 3, AlarmSeverity.MINOR, "4", "5")
+    assert det[alarm.KEY_MANAGED_OBJECT_ID] == "1"
+    assert det[alarm.KEY_APPLICATION_ID] == "2"
+    assert det[alarm.KEY_SPECIFIC_PROBLEM] == 3
+    assert det[alarm.KEY_PERCEIVED_SEVERITY] == AlarmSeverity.MINOR.name
+    assert det[alarm.KEY_IDENTIFYING_INFO] == "4"
+    assert det[alarm.KEY_ADDITIONAL_INFO] == "5"
+
+    mgr = alarm.AlarmManager(MRC_SEND, "moid2", "appid2")
+    assert mgr is not None
+    assert mgr.managed_object_id == "moid2"
+    assert mgr.application_id == "appid2"
+
+
+def _receive_alarm_msg(action: AlarmAction):
+    """
+    delays briefly, receives a message, checks the message type and action
+    """
+    time.sleep(0.5)
+    sbuf_rcv = rmr.rmr_alloc_msg(MRC_RCV, SIZE)
+    sbuf_rcv = rmr.rmr_torcv_msg(MRC_RCV, sbuf_rcv, 2000)
+    rcv_summary = rmr.message_summary(sbuf_rcv)
+    assert rcv_summary[rmr.RMR_MS_MSG_STATE] == rmr.RMR_OK
+    assert rcv_summary[rmr.RMR_MS_MSG_TYPE] == alarm.RIC_ALARM_UPDATE
+    # parse JSON
+    data = json.loads(rcv_summary[rmr.RMR_MS_PAYLOAD].decode())
+    assert data[alarm.KEY_ALARM_ACTION] == action.name
+
+
+def test_alarm_manager():
+    """
+    test send functions and ensure a message arrives
+    """
+    mgr = AlarmManager(MRC_SEND, "moid", "appid")
+    assert mgr is not None
+
+    det = mgr.create_alarm(3, AlarmSeverity.DEFAULT, "identifying", "additional")
+    assert det is not None
+
+    success = mgr.raise_alarm(det)
+    assert success
+    _receive_alarm_msg(AlarmAction.RAISE)
+
+    success = mgr.clear_alarm(det)
+    assert success
+    _receive_alarm_msg(AlarmAction.CLEAR)
+
+    success = mgr.reraise_alarm(det)
+    assert success
+    _receive_alarm_msg(AlarmAction.CLEAR)
+    _receive_alarm_msg(AlarmAction.RAISE)
+
+    success = mgr.clear_all_alarms()
+    assert success
+    _receive_alarm_msg(AlarmAction.CLEARALL)
diff --git a/tox.ini b/tox.ini
index 82f70f6..d023409 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -30,6 +30,7 @@ setenv =
     RMR_ASYNC_CONN = 0
 
 commands =
+    # add the -s flag after pytest to show the logs immediately when they arrive instead of delaying.
     pytest --cov ricxappframe --cov-report xml --cov-report term-missing --cov-report html --cov-fail-under=70
     coverage xml -i