Merge "PTP: Change overall sync-state behavior"
This commit is contained in:
commit
1f736e33fe
14
.zuul.yaml
14
.zuul.yaml
@ -9,7 +9,7 @@
|
|||||||
check:
|
check:
|
||||||
jobs:
|
jobs:
|
||||||
- openstack-tox-linters
|
- openstack-tox-linters
|
||||||
- ptp-notification-tox-py36
|
- ptp-notification-tox-py39
|
||||||
- k8sapp-ptp-notification-tox-py39
|
- k8sapp-ptp-notification-tox-py39
|
||||||
- k8sapp-ptp-notification-tox-pylint
|
- k8sapp-ptp-notification-tox-pylint
|
||||||
- k8sapp-ptp-notification-tox-flake8
|
- k8sapp-ptp-notification-tox-flake8
|
||||||
@ -17,7 +17,7 @@
|
|||||||
gate:
|
gate:
|
||||||
jobs:
|
jobs:
|
||||||
- openstack-tox-linters
|
- openstack-tox-linters
|
||||||
- ptp-notification-tox-py36
|
- ptp-notification-tox-py39
|
||||||
- k8sapp-ptp-notification-tox-py39
|
- k8sapp-ptp-notification-tox-py39
|
||||||
- k8sapp-ptp-notification-tox-pylint
|
- k8sapp-ptp-notification-tox-pylint
|
||||||
- k8sapp-ptp-notification-tox-flake8
|
- k8sapp-ptp-notification-tox-flake8
|
||||||
@ -27,13 +27,13 @@
|
|||||||
- stx-ptp-notification-armada-app-upload-git-mirror
|
- stx-ptp-notification-armada-app-upload-git-mirror
|
||||||
|
|
||||||
- job:
|
- job:
|
||||||
name: ptp-notification-tox-py36
|
name: ptp-notification-tox-py39
|
||||||
parent: tox-py36
|
parent: tox-py39
|
||||||
description: |
|
description: |
|
||||||
Run py36 test for ptp-notification
|
Run py39 test for ptp-notification
|
||||||
nodeset: ubuntu-bionic
|
nodeset: debian-bullseye
|
||||||
vars:
|
vars:
|
||||||
tox_envlist: py36
|
tox_envlist: py39
|
||||||
|
|
||||||
- job:
|
- job:
|
||||||
name: k8sapp-ptp-notification-tox-py39
|
name: k8sapp-ptp-notification-tox-py39
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021-2024 Wind River Systems, Inc.
|
# Copyright (c) 2021-2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -81,3 +81,8 @@ SOURCE_SYNC_SYNC_STATE = '/sync/sync-status/sync-state'
|
|||||||
SOURCE_SYNCE_CLOCK_QUALITY = '/sync/synce-status/clock-quality'
|
SOURCE_SYNCE_CLOCK_QUALITY = '/sync/synce-status/clock-quality'
|
||||||
SOURCE_SYNCE_LOCK_STATE_EXTENDED = '/sync/synce-status/lock-state-extended'
|
SOURCE_SYNCE_LOCK_STATE_EXTENDED = '/sync/synce-status/lock-state-extended'
|
||||||
SOURCE_SYNCE_LOCK_STATE = '/sync/synce-status/lock-state'
|
SOURCE_SYNCE_LOCK_STATE = '/sync/synce-status/lock-state'
|
||||||
|
|
||||||
|
class ClockSourceType(object):
|
||||||
|
TypePTP = "PTP"
|
||||||
|
TypeGNSS = "GNSS"
|
||||||
|
TypeNA = "NA"
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2022-2023 Wind River Systems, Inc.
|
# Copyright (c) 2022-2023,2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -12,6 +12,7 @@ from abc import ABC, abstractmethod
|
|||||||
|
|
||||||
from trackingfunctionsdk.common.helpers import constants
|
from trackingfunctionsdk.common.helpers import constants
|
||||||
from trackingfunctionsdk.common.helpers import log_helper
|
from trackingfunctionsdk.common.helpers import log_helper
|
||||||
|
from trackingfunctionsdk.common.helpers import ptpsync as utils
|
||||||
from trackingfunctionsdk.common.helpers.cgu_handler import CguHandler
|
from trackingfunctionsdk.common.helpers.cgu_handler import CguHandler
|
||||||
from trackingfunctionsdk.model.dto.gnssstate import GnssState
|
from trackingfunctionsdk.model.dto.gnssstate import GnssState
|
||||||
|
|
||||||
@ -48,6 +49,7 @@ class GnssMonitor(Observer):
|
|||||||
"GnssMonitor: Unable to determine tsphc_service name from %s"
|
"GnssMonitor: Unable to determine tsphc_service name from %s"
|
||||||
% self.config_file)
|
% self.config_file)
|
||||||
|
|
||||||
|
self.set_ptp_devices()
|
||||||
# Setup GNSS data
|
# Setup GNSS data
|
||||||
self.gnss_cgu_handler = CguHandler(config_file, nmea_serialport,
|
self.gnss_cgu_handler = CguHandler(config_file, nmea_serialport,
|
||||||
pci_addr, cgu_path)
|
pci_addr, cgu_path)
|
||||||
@ -78,6 +80,34 @@ class GnssMonitor(Observer):
|
|||||||
self.gnss_pps_state = \
|
self.gnss_pps_state = \
|
||||||
self.gnss_cgu_handler.cgu_output_parsed['PPS DPLL']['Status']
|
self.gnss_cgu_handler.cgu_output_parsed['PPS DPLL']['Status']
|
||||||
|
|
||||||
|
def set_ptp_devices(self):
|
||||||
|
ptp_devices = set()
|
||||||
|
phc_interfaces = self._check_config_file_interfaces()
|
||||||
|
for phc_interface in phc_interfaces:
|
||||||
|
ptp_device = utils.get_interface_phc_device(phc_interface)
|
||||||
|
if ptp_device is not None:
|
||||||
|
ptp_devices.add(ptp_device)
|
||||||
|
self.ptp_devices = list(ptp_devices)
|
||||||
|
LOG.debug("TS2PHC PTP devices are %s" % self.ptp_devices)
|
||||||
|
|
||||||
|
def get_ptp_devices(self):
|
||||||
|
return self.ptp_devices
|
||||||
|
|
||||||
|
def _check_config_file_interfaces(self):
|
||||||
|
phc_interfaces = []
|
||||||
|
with open(self.config_file, 'r') as f:
|
||||||
|
config_lines = f.readlines()
|
||||||
|
config_lines = [line.rstrip() for line in config_lines]
|
||||||
|
|
||||||
|
for line in config_lines:
|
||||||
|
# Find the interface value inside the square brackets
|
||||||
|
if re.match(r"^\[.*\]$", line) and line != "[global]":
|
||||||
|
phc_interface = line.strip("[]")
|
||||||
|
LOG.debug("TS2PHC interface is %s" % phc_interface)
|
||||||
|
phc_interfaces.append(phc_interface)
|
||||||
|
|
||||||
|
return phc_interfaces
|
||||||
|
|
||||||
def update(self, subject, matched_line) -> None:
|
def update(self, subject, matched_line) -> None:
|
||||||
LOG.info("Kernel event detected. %s" % matched_line)
|
LOG.info("Kernel event detected. %s" % matched_line)
|
||||||
self.set_gnss_status()
|
self.set_gnss_status()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2022-2023 Wind River Systems, Inc.
|
# Copyright (c) 2022-2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -249,26 +249,7 @@ class OsClockMonitor:
|
|||||||
|
|
||||||
def _get_interface_phc_device(self):
|
def _get_interface_phc_device(self):
|
||||||
"""Determine the phc device for the interface"""
|
"""Determine the phc device for the interface"""
|
||||||
pattern = "/hostsys/class/net/" + self.phc_interface + "/device/ptp/*"
|
return utils.get_interface_phc_device(self.phc_interface)
|
||||||
ptp_device = glob(pattern)
|
|
||||||
if len(ptp_device) == 0:
|
|
||||||
# Try the 0th interface instead, required for some NIC types
|
|
||||||
phc_interface_base = self.phc_interface[:-1] + "0"
|
|
||||||
LOG.info("No ptp device found at %s trying %s instead"
|
|
||||||
% (pattern, phc_interface_base))
|
|
||||||
pattern = "/hostsys/class/net/" + phc_interface_base + \
|
|
||||||
"/device/ptp/*"
|
|
||||||
ptp_device = glob(pattern)
|
|
||||||
if len(ptp_device) == 0:
|
|
||||||
LOG.warning("No ptp device found for base interface at %s"
|
|
||||||
% pattern)
|
|
||||||
return None
|
|
||||||
if len(ptp_device) > 1:
|
|
||||||
LOG.error("More than one ptp device found at %s" % pattern)
|
|
||||||
return None
|
|
||||||
ptp_device = os.path.basename(ptp_device[0])
|
|
||||||
LOG.debug("Found ptp device %s at %s" % (ptp_device, pattern))
|
|
||||||
return ptp_device
|
|
||||||
|
|
||||||
def get_os_clock_offset(self):
|
def get_os_clock_offset(self):
|
||||||
"""Get the os CLOCK_REALTIME offset"""
|
"""Get the os CLOCK_REALTIME offset"""
|
||||||
@ -327,6 +308,12 @@ class OsClockMonitor:
|
|||||||
def get_os_clock_state(self):
|
def get_os_clock_state(self):
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
def get_source_ptp_device(self):
|
||||||
|
# PTP device that is disciplining the OS clock
|
||||||
|
# This is also valid in case of HA source devices as
|
||||||
|
# __publish_os_clock_status updates ptp_device.
|
||||||
|
return self.ptp_device
|
||||||
|
|
||||||
def os_clock_status(self, holdover_time, freq, sync_state, event_time):
|
def os_clock_status(self, holdover_time, freq, sync_state, event_time):
|
||||||
current_time = datetime.datetime.utcnow().timestamp()
|
current_time = datetime.datetime.utcnow().timestamp()
|
||||||
time_in_holdover = None
|
time_in_holdover = None
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021-2024 Wind River Systems, Inc.
|
# Copyright (c) 2021-2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -13,6 +13,7 @@
|
|||||||
#
|
#
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from trackingfunctionsdk.model.dto.ptpstate import PtpState
|
from trackingfunctionsdk.model.dto.ptpstate import PtpState
|
||||||
@ -62,12 +63,48 @@ class PtpMonitor:
|
|||||||
self.phc2sys_service_name = phc2sys_service_name
|
self.phc2sys_service_name = phc2sys_service_name
|
||||||
self.holdover_time = int(holdover_time)
|
self.holdover_time = int(holdover_time)
|
||||||
self.freq = int(freq)
|
self.freq = int(freq)
|
||||||
|
self.set_ptp_devices()
|
||||||
|
self.sync_source = constants.ClockSourceType.TypeNA
|
||||||
self._ptp_event_time = datetime.datetime.utcnow().timestamp()
|
self._ptp_event_time = datetime.datetime.utcnow().timestamp()
|
||||||
self._clock_class_event_time = \
|
self._clock_class_event_time = \
|
||||||
datetime.datetime.utcnow().timestamp()
|
datetime.datetime.utcnow().timestamp()
|
||||||
self.set_ptp_sync_state()
|
self.set_ptp_sync_state()
|
||||||
self.set_ptp_clock_class()
|
self.set_ptp_clock_class()
|
||||||
|
|
||||||
|
def set_ptp_devices(self):
|
||||||
|
ptp_devices = set()
|
||||||
|
phc_interfaces = self._check_config_file_interfaces()
|
||||||
|
for phc_interface in phc_interfaces:
|
||||||
|
ptp_device = utils.get_interface_phc_device(phc_interface)
|
||||||
|
if ptp_device is not None:
|
||||||
|
ptp_devices.add(ptp_device)
|
||||||
|
self.ptp_devices = list(ptp_devices)
|
||||||
|
LOG.debug("PTP4l PTP devices are %s" % self.ptp_devices)
|
||||||
|
|
||||||
|
def get_ptp_devices(self):
|
||||||
|
return self.ptp_devices
|
||||||
|
|
||||||
|
def get_ptp_sync_source(self):
|
||||||
|
return self.sync_source
|
||||||
|
|
||||||
|
def _check_config_file_interfaces(self):
|
||||||
|
phc_interfaces = []
|
||||||
|
with open(self.ptp4l_config, 'r') as f:
|
||||||
|
config_lines = f.readlines()
|
||||||
|
config_lines = [line.rstrip() for line in config_lines]
|
||||||
|
|
||||||
|
for line in config_lines:
|
||||||
|
# Find the interface value inside the square brackets
|
||||||
|
if re.match(r"^\[.*\]$", line) and line not in [
|
||||||
|
"[global]",
|
||||||
|
"[unicast_master_table]",
|
||||||
|
]:
|
||||||
|
phc_interface = line.strip("[]")
|
||||||
|
LOG.debug("PTP4l interface is %s" % phc_interface)
|
||||||
|
phc_interfaces.append(phc_interface)
|
||||||
|
|
||||||
|
return phc_interfaces
|
||||||
|
|
||||||
def set_ptp_sync_state(self):
|
def set_ptp_sync_state(self):
|
||||||
new_ptp_sync_event, ptp_sync_state, ptp_event_time = self.ptp_status()
|
new_ptp_sync_event, ptp_sync_state, ptp_event_time = self.ptp_status()
|
||||||
if ptp_sync_state != self._ptp_sync_state:
|
if ptp_sync_state != self._ptp_sync_state:
|
||||||
@ -132,6 +169,9 @@ class PtpMonitor:
|
|||||||
# max holdover time is calculated to be in a 'safety' zone
|
# max holdover time is calculated to be in a 'safety' zone
|
||||||
max_holdover_time = (self.holdover_time - self.freq * 2)
|
max_holdover_time = (self.holdover_time - self.freq * 2)
|
||||||
|
|
||||||
|
previous_sync_source = self.sync_source
|
||||||
|
sync_source = constants.ClockSourceType.TypeNA
|
||||||
|
|
||||||
pmc, ptp4l, _, ptp4lconf = \
|
pmc, ptp4l, _, ptp4lconf = \
|
||||||
utils.check_critical_resources(self.ptp4l_service_name,
|
utils.check_critical_resources(self.ptp4l_service_name,
|
||||||
self.phc2sys_service_name)
|
self.phc2sys_service_name)
|
||||||
@ -141,11 +181,13 @@ class PtpMonitor:
|
|||||||
self.pmc_query_results, total_ptp_keywords, port_count = \
|
self.pmc_query_results, total_ptp_keywords, port_count = \
|
||||||
self.ptpsync()
|
self.ptpsync()
|
||||||
try:
|
try:
|
||||||
sync_state = utils.check_results(self.pmc_query_results,
|
sync_state, sync_source = utils.check_results(
|
||||||
total_ptp_keywords, port_count)
|
self.pmc_query_results,total_ptp_keywords, port_count
|
||||||
|
)
|
||||||
except RuntimeError as err:
|
except RuntimeError as err:
|
||||||
LOG.warning(err)
|
LOG.warning(err)
|
||||||
sync_state = previous_sync_state
|
sync_state = previous_sync_state
|
||||||
|
sync_source = previous_sync_source
|
||||||
else:
|
else:
|
||||||
LOG.warning("Missing critical resource: "
|
LOG.warning("Missing critical resource: "
|
||||||
"PMC %s PTP4L %s PTP4LCONF %s"
|
"PMC %s PTP4L %s PTP4LCONF %s"
|
||||||
@ -169,7 +211,16 @@ class PtpMonitor:
|
|||||||
|
|
||||||
# determine if ptp sync state has changed since the last one
|
# determine if ptp sync state has changed since the last one
|
||||||
LOG.debug("ptp_monitor: sync_state %s, "
|
LOG.debug("ptp_monitor: sync_state %s, "
|
||||||
"previous_sync_state %s" % (sync_state, previous_sync_state))
|
"previous_sync_state %s, "
|
||||||
|
"sync_source %s, "
|
||||||
|
"previous sync_source %s" % (
|
||||||
|
sync_state, previous_sync_state, sync_source, previous_sync_source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# record sync_source of this poll.
|
||||||
|
self.sync_source = sync_source
|
||||||
|
|
||||||
if sync_state != previous_sync_state:
|
if sync_state != previous_sync_state:
|
||||||
new_event = True
|
new_event = True
|
||||||
self._ptp_event_time = datetime.datetime.utcnow().timestamp()
|
self._ptp_event_time = datetime.datetime.utcnow().timestamp()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#! /usr/bin/python3
|
#! /usr/bin/python3
|
||||||
#
|
#
|
||||||
# Copyright (c) 2021-2024 Wind River Systems, Inc.
|
# Copyright (c) 2021-2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -16,6 +16,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
from glob import glob
|
||||||
from trackingfunctionsdk.common.helpers import constants
|
from trackingfunctionsdk.common.helpers import constants
|
||||||
from trackingfunctionsdk.common.helpers import log_helper
|
from trackingfunctionsdk.common.helpers import log_helper
|
||||||
|
|
||||||
@ -69,6 +70,9 @@ def check_results(result, total_ptp_keywords, port_count):
|
|||||||
# sync state is in 'Locked' state and will be overwritten if
|
# sync state is in 'Locked' state and will be overwritten if
|
||||||
# it is not the case
|
# it is not the case
|
||||||
sync_state = constants.LOCKED_PHC_STATE
|
sync_state = constants.LOCKED_PHC_STATE
|
||||||
|
# sync source is in 'NA' and will be overwritten when source
|
||||||
|
# found to be GNSS or PTP.
|
||||||
|
sync_source = constants.ClockSourceType.TypeNA
|
||||||
|
|
||||||
local_gm = False
|
local_gm = False
|
||||||
|
|
||||||
@ -77,16 +81,18 @@ def check_results(result, total_ptp_keywords, port_count):
|
|||||||
LOG.info("Results %s" % result)
|
LOG.info("Results %s" % result)
|
||||||
LOG.info("Results len %s, total_ptp_keywords %s" % (len(result), total_ptp_keywords))
|
LOG.info("Results len %s, total_ptp_keywords %s" % (len(result), total_ptp_keywords))
|
||||||
raise RuntimeError("PMC results are incomplete, retrying")
|
raise RuntimeError("PMC results are incomplete, retrying")
|
||||||
# determine the current sync state
|
# determine the current sync state and sync source
|
||||||
if (result[constants.GM_PRESENT].lower() != constants.GM_IS_PRESENT
|
if (result[constants.GM_PRESENT].lower() != constants.GM_IS_PRESENT
|
||||||
and result[constants.GRANDMASTER_IDENTITY] != result[constants.CLOCK_IDENTITY]):
|
and result[constants.GRANDMASTER_IDENTITY] != result[constants.CLOCK_IDENTITY]):
|
||||||
sync_state = constants.FREERUN_PHC_STATE
|
sync_state = constants.FREERUN_PHC_STATE
|
||||||
elif result[constants.GRANDMASTER_IDENTITY] == result[constants.CLOCK_IDENTITY]:
|
elif result[constants.GRANDMASTER_IDENTITY] == result[constants.CLOCK_IDENTITY]:
|
||||||
local_gm = True
|
local_gm = True
|
||||||
|
sync_source = constants.ClockSourceType.TypeGNSS
|
||||||
LOG.debug("Local node is a GM")
|
LOG.debug("Local node is a GM")
|
||||||
if not local_gm:
|
if not local_gm:
|
||||||
for port in range(1, port_count + 1):
|
for port in range(1, port_count + 1):
|
||||||
if result[constants.PORT.format(port)].lower() == constants.SLAVE_MODE:
|
if result[constants.PORT.format(port)].lower() == constants.SLAVE_MODE:
|
||||||
|
sync_source = constants.ClockSourceType.TypePTP
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
sync_state = constants.FREERUN_PHC_STATE
|
sync_state = constants.FREERUN_PHC_STATE
|
||||||
@ -101,7 +107,7 @@ def check_results(result, total_ptp_keywords, port_count):
|
|||||||
sync_state = constants.FREERUN_PHC_STATE
|
sync_state = constants.FREERUN_PHC_STATE
|
||||||
if (result[constants.GM_CLOCK_CLASS] not in ptp4l_clock_class_locked):
|
if (result[constants.GM_CLOCK_CLASS] not in ptp4l_clock_class_locked):
|
||||||
sync_state = constants.FREERUN_PHC_STATE
|
sync_state = constants.FREERUN_PHC_STATE
|
||||||
return sync_state
|
return sync_state, sync_source
|
||||||
|
|
||||||
|
|
||||||
def parse_resource_address(resource_address):
|
def parse_resource_address(resource_address):
|
||||||
@ -123,3 +129,27 @@ def format_resource_address(node_name, resource, instance=None):
|
|||||||
resource_address = resource_address + resource
|
resource_address = resource_address + resource
|
||||||
LOG.debug("format_resource_address %s" % resource_address)
|
LOG.debug("format_resource_address %s" % resource_address)
|
||||||
return resource_address
|
return resource_address
|
||||||
|
|
||||||
|
|
||||||
|
def get_interface_phc_device(phc_interface):
|
||||||
|
"""Determine the phc device for the interface"""
|
||||||
|
pattern = "/hostsys/class/net/" + phc_interface + "/device/ptp/*"
|
||||||
|
ptp_device = glob(pattern)
|
||||||
|
if len(ptp_device) == 0:
|
||||||
|
# Try the 0th interface instead, required for some NIC types
|
||||||
|
phc_interface_base = phc_interface[:-1] + "0"
|
||||||
|
LOG.info("No ptp device found at %s trying %s instead"
|
||||||
|
% (pattern, phc_interface_base))
|
||||||
|
pattern = "/hostsys/class/net/" + phc_interface_base + \
|
||||||
|
"/device/ptp/*"
|
||||||
|
ptp_device = glob(pattern)
|
||||||
|
if len(ptp_device) == 0:
|
||||||
|
LOG.warning("No ptp device found for base interface at %s"
|
||||||
|
% pattern)
|
||||||
|
return None
|
||||||
|
if len(ptp_device) > 1:
|
||||||
|
LOG.error("More than one ptp device found at %s" % pattern)
|
||||||
|
return None
|
||||||
|
ptp_device = os.path.basename(ptp_device[0])
|
||||||
|
LOG.debug("Found ptp device %s at %s" % (ptp_device, pattern))
|
||||||
|
return ptp_device
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021-2024 Wind River Systems, Inc.
|
# Copyright (c) 2021-2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -466,8 +466,51 @@ class PtpWatcherDefault:
|
|||||||
LOG.debug("Getting os clock status.")
|
LOG.debug("Getting os clock status.")
|
||||||
return new_event, sync_state, new_event_time
|
return new_event, sync_state, new_event_time
|
||||||
|
|
||||||
def __get_overall_sync_state(self, holdover_time, freq, sync_state,
|
def __get_primary_ptp_state(self, ptp_device):
|
||||||
last_event_time):
|
# The PTP device itself is being disciplined or not ?
|
||||||
|
# Check which ptp4l instance disciplining this PTP device
|
||||||
|
# disciplining source could be either GNSS or PTP
|
||||||
|
primary_ptp4l = None
|
||||||
|
ptp_state = PtpState.Freerun
|
||||||
|
for ptp4l in self.ptp_monitor_list:
|
||||||
|
# runtime loading of ptp4l config
|
||||||
|
ptp4l.set_ptp_devices()
|
||||||
|
if (
|
||||||
|
ptp_device in ptp4l.get_ptp_devices()
|
||||||
|
and ptp4l.get_ptp_sync_source() != constants.ClockSourceType.TypeNA
|
||||||
|
):
|
||||||
|
primary_ptp4l = ptp4l
|
||||||
|
break
|
||||||
|
|
||||||
|
if primary_ptp4l is not None:
|
||||||
|
_, read_state, _ = primary_ptp4l.get_ptp_sync_state()
|
||||||
|
if read_state == PtpState.Locked:
|
||||||
|
ptp_state = PtpState.Locked
|
||||||
|
|
||||||
|
return primary_ptp4l, ptp_state
|
||||||
|
|
||||||
|
def __get_primary_gnss_state(self, ptp_device):
|
||||||
|
# The PTP device itself is being disciplined or not ?
|
||||||
|
# Check which ts2phc instance disciplining this PTP device
|
||||||
|
primary_gnss = None
|
||||||
|
gnss_state = GnssState.Failure_Nofix
|
||||||
|
for gnss in self.observer_list:
|
||||||
|
# runtime loading of ts2phc config
|
||||||
|
gnss.set_ptp_devices()
|
||||||
|
if ptp_device in gnss.get_ptp_devices():
|
||||||
|
primary_gnss = gnss
|
||||||
|
break
|
||||||
|
|
||||||
|
if primary_gnss is not None:
|
||||||
|
read_state = primary_gnss._state
|
||||||
|
if read_state == GnssState.Synchronized:
|
||||||
|
gnss_state = GnssState.Synchronized
|
||||||
|
|
||||||
|
return primary_gnss, gnss_state
|
||||||
|
|
||||||
|
def __get_overall_sync_state(
|
||||||
|
self, holdover_time, freq, sync_state, last_event_time
|
||||||
|
):
|
||||||
new_event = False
|
new_event = False
|
||||||
new_event_time = last_event_time
|
new_event_time = last_event_time
|
||||||
previous_sync_state = sync_state
|
previous_sync_state = sync_state
|
||||||
@ -481,45 +524,91 @@ class PtpWatcherDefault:
|
|||||||
ptp_state = None
|
ptp_state = None
|
||||||
|
|
||||||
LOG.debug("Getting overall sync state.")
|
LOG.debug("Getting overall sync state.")
|
||||||
for gnss in self.observer_list:
|
|
||||||
if gnss._state == constants.UNKNOWN_PHC_STATE or \
|
|
||||||
gnss._state == GnssState.Failure_Nofix:
|
|
||||||
gnss_state = GnssState.Failure_Nofix
|
|
||||||
elif gnss._state == GnssState.Synchronized and \
|
|
||||||
gnss_state != GnssState.Failure_Nofix:
|
|
||||||
gnss_state = GnssState.Synchronized
|
|
||||||
|
|
||||||
for ptp4l in self.ptp_monitor_list:
|
|
||||||
_, read_state, _ = ptp4l.get_ptp_sync_state()
|
|
||||||
if read_state == PtpState.Holdover or \
|
|
||||||
read_state == PtpState.Freerun or \
|
|
||||||
read_state == constants.UNKNOWN_PHC_STATE:
|
|
||||||
ptp_state = PtpState.Freerun
|
|
||||||
elif read_state == PtpState.Locked and \
|
|
||||||
ptp_state != PtpState.Freerun:
|
|
||||||
ptp_state = PtpState.Locked
|
|
||||||
|
|
||||||
|
# overall state depends on os_clock_state and single chained gnss/ptp state
|
||||||
|
# Need to figure out which gnss/ptp is disciplining the PHC that syncs os_clock
|
||||||
os_clock_state = self.os_clock_monitor.get_os_clock_state()
|
os_clock_state = self.os_clock_monitor.get_os_clock_state()
|
||||||
|
sync_state = OverallClockState.Freerun
|
||||||
|
if os_clock_state is not OsClockState.Freerun:
|
||||||
|
# PTP device that is disciplining the OS clock,
|
||||||
|
# valid even for HA source devices
|
||||||
|
ptp_device = self.os_clock_monitor.get_source_ptp_device()
|
||||||
|
if ptp_device is None:
|
||||||
|
# This may happen in virtualized environments
|
||||||
|
LOG.warning("No PTP device. Defaulting overall state Freerun")
|
||||||
|
else:
|
||||||
|
# What source (gnss or ptp) disciplining the PTP device at the
|
||||||
|
# moment (A PTP device could have both TS2PHC/gnss source and
|
||||||
|
# PTP4l/slave)
|
||||||
|
sync_source = constants.ClockSourceType.TypeNA
|
||||||
|
# any ts2phc instance disciplining the ptp device (source GNSS)
|
||||||
|
primary_gnss, gnss_state = self.__get_primary_gnss_state(ptp_device)
|
||||||
|
if primary_gnss is not None:
|
||||||
|
sync_source = constants.ClockSourceType.TypeGNSS
|
||||||
|
|
||||||
if gnss_state is GnssState.Failure_Nofix or \
|
# any ptp4l instance disciplining the ptp device (source PTP or GNSS)
|
||||||
os_clock_state is OsClockState.Freerun or \
|
primary_ptp4l, ptp_state = self.__get_primary_ptp_state(ptp_device)
|
||||||
ptp_state is PtpState.Freerun:
|
|
||||||
sync_state = OverallClockState.Freerun
|
# which source: PTP or GNSS
|
||||||
else:
|
# In presence of ptp4l instance disciplining the ptp device, it truly
|
||||||
sync_state = OverallClockState.Locked
|
# dictates what source it is using.
|
||||||
|
if primary_ptp4l is not None:
|
||||||
|
sync_source = primary_ptp4l.get_ptp_sync_source()
|
||||||
|
|
||||||
|
ptp4l_instance_and_state = (
|
||||||
|
"NA"
|
||||||
|
if primary_ptp4l is None
|
||||||
|
else (primary_ptp4l.ptp4l_service_name, ptp_state)
|
||||||
|
)
|
||||||
|
ts2phc_instance_and_state = (
|
||||||
|
"NA"
|
||||||
|
if primary_gnss is None
|
||||||
|
else (primary_gnss.ts2phc_service_name, gnss_state)
|
||||||
|
)
|
||||||
|
LOG.debug(
|
||||||
|
f"Overall sync state chaining info:\n"
|
||||||
|
f"os-clock's source ptp-device = {ptp_device}\n"
|
||||||
|
f"ptp-device's sync-source = {sync_source}\n"
|
||||||
|
f"ptp4l-instance-and-state = {ptp4l_instance_and_state}\n"
|
||||||
|
f"ts2phc-instance-and-state = {ts2phc_instance_and_state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Based on sync_source that is used to discipline the ptp device,
|
||||||
|
# dependent ts2phc or ptp4l instance's state is chosen.
|
||||||
|
if sync_source == constants.ClockSourceType.TypeNA:
|
||||||
|
# The PTP device is not being disciplined by any PTP4l/TS2PHC instances
|
||||||
|
LOG.warning(
|
||||||
|
"PTP device used by PHC2SYS is not synced/configured on any PTP4l/TS2PHC instances."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
sync_source == constants.ClockSourceType.TypeGNSS
|
||||||
|
and gnss_state is GnssState.Synchronized
|
||||||
|
):
|
||||||
|
sync_state = OverallClockState.Locked
|
||||||
|
|
||||||
|
elif (
|
||||||
|
sync_source == constants.ClockSourceType.TypePTP
|
||||||
|
and ptp_state is PtpState.Locked
|
||||||
|
):
|
||||||
|
sync_state = OverallClockState.Locked
|
||||||
|
|
||||||
if sync_state == OverallClockState.Freerun:
|
if sync_state == OverallClockState.Freerun:
|
||||||
if previous_sync_state in [
|
if previous_sync_state in [
|
||||||
constants.UNKNOWN_PHC_STATE,
|
constants.UNKNOWN_PHC_STATE,
|
||||||
constants.FREERUN_PHC_STATE]:
|
constants.FREERUN_PHC_STATE,
|
||||||
|
]:
|
||||||
sync_state = OverallClockState.Freerun
|
sync_state = OverallClockState.Freerun
|
||||||
elif previous_sync_state == constants.LOCKED_PHC_STATE:
|
elif previous_sync_state == constants.LOCKED_PHC_STATE:
|
||||||
sync_state = OverallClockState.Holdover
|
sync_state = OverallClockState.Holdover
|
||||||
elif previous_sync_state == constants.HOLDOVER_PHC_STATE and \
|
elif (
|
||||||
time_in_holdover < max_holdover_time:
|
previous_sync_state == constants.HOLDOVER_PHC_STATE
|
||||||
LOG.debug("Overall sync: Time in holdover is %s "
|
and time_in_holdover < max_holdover_time
|
||||||
"Max time in holdover is %s"
|
):
|
||||||
% (time_in_holdover, max_holdover_time))
|
LOG.debug(
|
||||||
|
"Overall sync: Time in holdover is %s "
|
||||||
|
"Max time in holdover is %s" % (time_in_holdover, max_holdover_time)
|
||||||
|
)
|
||||||
sync_state = OverallClockState.Holdover
|
sync_state = OverallClockState.Holdover
|
||||||
else:
|
else:
|
||||||
sync_state = OverallClockState.Freerun
|
sync_state = OverallClockState.Freerun
|
||||||
|
@ -0,0 +1,517 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
import mock
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from trackingfunctionsdk.common.helpers import constants
|
||||||
|
from trackingfunctionsdk.model.dto.osclockstate import OsClockState
|
||||||
|
from trackingfunctionsdk.model.dto.overallclockstate import OverallClockState
|
||||||
|
from trackingfunctionsdk.model.dto.ptpstate import PtpState
|
||||||
|
from trackingfunctionsdk.model.dto.gnssstate import GnssState
|
||||||
|
from trackingfunctionsdk.services.daemon import PtpWatcherDefault
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"THIS_NAMESPACE": "notification",
|
||||||
|
"THIS_NODE_NAME": "controller-0",
|
||||||
|
"THIS_POD_IP": "172.16.192.71",
|
||||||
|
"REGISTRATION_TRANSPORT_ENDPOINT": "rabbit://admin:admin@registration.notification.svc.cluster.local:5672",
|
||||||
|
"NOTIFICATION_TRANSPORT_ENDPOINT": "rabbit://admin:admin@172.16.192.71:5672",
|
||||||
|
"GNSS_CONFIGS": [
|
||||||
|
"/ptp/linuxptp/ptpinstance/ts2phc-ts1.conf",
|
||||||
|
"/ptp/linuxptp/ptpinstance/ts2phc-ts2.conf",
|
||||||
|
],
|
||||||
|
"PHC2SYS_CONFIG": "/ptp/linuxptp/ptpinstance/phc2sys-phc-inst1.conf",
|
||||||
|
"PHC2SYS_SERVICE_NAME": "phc-inst1",
|
||||||
|
"PTP4L_CONFIGS": [
|
||||||
|
"/ptp/linuxptp/ptpinstance/ptp4l-ptp-inst2.conf",
|
||||||
|
"/ptp/linuxptp/ptpinstance/ptp4l-ptp-inst1.conf",
|
||||||
|
],
|
||||||
|
"GNSS_INSTANCES": ["ts1", "ts2"],
|
||||||
|
"PTP4L_INSTANCES": ["ptp-inst2", "ptp-inst1"],
|
||||||
|
"ptptracker_context": {"device_simulated": "false", "holdover_seconds": "15"},
|
||||||
|
"gnsstracker_context": {"holdover_seconds": 30},
|
||||||
|
"osclocktracker_context": {"holdover_seconds": "15"},
|
||||||
|
"overalltracker_context": {"holdover_seconds": "15"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OsClockData:
|
||||||
|
sync_state: str = OsClockState.Freerun
|
||||||
|
sync_source: str = "ptp0"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PTP4lData:
|
||||||
|
ptp_devices: list[str]
|
||||||
|
sync_state: str = PtpState.Freerun
|
||||||
|
sync_source: str = constants.ClockSourceType.TypePTP
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ts2phcData:
|
||||||
|
ptp_devices: list[str]
|
||||||
|
sync_state: str = GnssState.Failure_Nofix
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestData:
|
||||||
|
osclock: OsClockData
|
||||||
|
ptp4l: list[PTP4lData]
|
||||||
|
ts2phc: list[ts2phcData]
|
||||||
|
|
||||||
|
|
||||||
|
class DaemonTests(unittest.TestCase):
|
||||||
|
|
||||||
|
@mock.patch("trackingfunctionsdk.services.daemon.PtpMonitor")
|
||||||
|
@mock.patch("trackingfunctionsdk.services.daemon.OsClockMonitor")
|
||||||
|
@mock.patch("trackingfunctionsdk.services.daemon.GnssMonitor")
|
||||||
|
def _setup(self, gnssmonitor_mock, osclockmonitor_mock, ptpmonitor_mock):
|
||||||
|
event = None
|
||||||
|
|
||||||
|
sqlalchemy_conf = {
|
||||||
|
"url": "sqlite:///apiserver.db",
|
||||||
|
"echo": False,
|
||||||
|
"echo_pool": False,
|
||||||
|
"pool_recycle": 3600,
|
||||||
|
"encoding": "utf-8",
|
||||||
|
}
|
||||||
|
sqlalchemy_conf_json = json.dumps(sqlalchemy_conf)
|
||||||
|
daemon_context_json = json.dumps(context)
|
||||||
|
|
||||||
|
# distint mock class instances, to have distinct mock method on instance basis
|
||||||
|
gnssmonitor_mock.side_effect = [
|
||||||
|
mock.Mock(name=item) for item in context["GNSS_CONFIGS"]
|
||||||
|
]
|
||||||
|
ptpmonitor_mock.side_effect = [
|
||||||
|
mock.Mock(name=item) for item in context["PTP4L_CONFIGS"]
|
||||||
|
]
|
||||||
|
|
||||||
|
self.worker = PtpWatcherDefault(
|
||||||
|
event, sqlalchemy_conf_json, daemon_context_json
|
||||||
|
)
|
||||||
|
|
||||||
|
self.osclockmonitor_mock_instance = self.worker.os_clock_monitor
|
||||||
|
self.ptpmonitor_mock_instances = self.worker.ptp_monitor_list
|
||||||
|
self.gnssmonitor_mock_instances = self.worker.observer_list
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(self.gnssmonitor_mock_instances), len(context["GNSS_CONFIGS"])
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
len(self.ptpmonitor_mock_instances), len(context["PTP4L_CONFIGS"])
|
||||||
|
)
|
||||||
|
|
||||||
|
def _test__get_overall_sync_state(self, testdata, expected):
|
||||||
|
holdover_time = float(context["overalltracker_context"]["holdover_seconds"])
|
||||||
|
freq = 2
|
||||||
|
sync_state = OverallClockState.Freerun
|
||||||
|
last_event_time = time.time()
|
||||||
|
|
||||||
|
self.osclockmonitor_mock_instance.get_source_ptp_device.return_value = (
|
||||||
|
testdata.osclock.sync_source
|
||||||
|
)
|
||||||
|
self.osclockmonitor_mock_instance.get_os_clock_state.return_value = (
|
||||||
|
testdata.osclock.sync_state
|
||||||
|
)
|
||||||
|
# test mocking as expected or not.
|
||||||
|
self.assertEqual(
|
||||||
|
self.worker.os_clock_monitor.get_source_ptp_device(),
|
||||||
|
testdata.osclock.sync_source,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.worker.os_clock_monitor.get_os_clock_state(),
|
||||||
|
testdata.osclock.sync_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, gnssmonitor_mock_instance in enumerate(self.gnssmonitor_mock_instances):
|
||||||
|
gnssmonitor_mock_instance.get_ptp_devices.return_value = testdata.ts2phc[
|
||||||
|
i
|
||||||
|
].ptp_devices
|
||||||
|
gnssmonitor_mock_instance._state = testdata.ts2phc[i].sync_state
|
||||||
|
# test mocking as expected or not.
|
||||||
|
self.assertEqual(
|
||||||
|
self.gnssmonitor_mock_instances[0].get_ptp_devices(),
|
||||||
|
testdata.ts2phc[0].ptp_devices,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.gnssmonitor_mock_instances[0]._state, testdata.ts2phc[0].sync_state
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.gnssmonitor_mock_instances[1].get_ptp_devices(),
|
||||||
|
testdata.ts2phc[1].ptp_devices,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.gnssmonitor_mock_instances[1]._state, testdata.ts2phc[1].sync_state
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, ptpmonitor_mock_instance in enumerate(self.ptpmonitor_mock_instances):
|
||||||
|
ptpmonitor_mock_instance.get_ptp_devices.return_value = testdata.ptp4l[
|
||||||
|
i
|
||||||
|
].ptp_devices
|
||||||
|
ptpmonitor_mock_instance.get_ptp_sync_state.return_value = (
|
||||||
|
None,
|
||||||
|
testdata.ptp4l[i].sync_state,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
ptpmonitor_mock_instance.get_ptp_sync_source.return_value = testdata.ptp4l[
|
||||||
|
i
|
||||||
|
].sync_source
|
||||||
|
# test mocking as expected or not.
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmonitor_mock_instances[0].get_ptp_devices(),
|
||||||
|
testdata.ptp4l[0].ptp_devices,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmonitor_mock_instances[0].get_ptp_sync_state(),
|
||||||
|
(None, testdata.ptp4l[0].sync_state, None),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmonitor_mock_instances[0].get_ptp_sync_source(),
|
||||||
|
testdata.ptp4l[0].sync_source,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmonitor_mock_instances[1].get_ptp_devices(),
|
||||||
|
testdata.ptp4l[1].ptp_devices,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmonitor_mock_instances[1].get_ptp_sync_state(),
|
||||||
|
(None, testdata.ptp4l[1].sync_state, None),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmonitor_mock_instances[1].get_ptp_sync_source(),
|
||||||
|
testdata.ptp4l[1].sync_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_event, sync_state, new_event_time = (
|
||||||
|
self.worker._PtpWatcherDefault__get_overall_sync_state(
|
||||||
|
holdover_time, freq, sync_state, last_event_time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# overall sync state assertion
|
||||||
|
self.assertEqual(sync_state, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__all_are_locked__overall_locked(self):
|
||||||
|
# when all are locked state -- overall state would be locked
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Locked
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__osclock_freerun__overall_freerun(self):
|
||||||
|
# when osclock is on freerun, and others are on locked state -- overall
|
||||||
|
# state would be freerun
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Freerun, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Freerun
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__ptp4l_ptp0_freerun__overall_freerun(self):
|
||||||
|
# when chained ptp4l ptp0 sync_state is freerun -- overall state would be freerun
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Freerun,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Freerun
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__ptp4l_ptp0_locked__overall_locked(self):
|
||||||
|
# when chained ptp4l ptp0 sync_state is locked -- overall state would be locked
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Freerun,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Failure_Nofix
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Failure_Nofix
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Locked
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__ts2phc_ptp0_freerun__overall_freerun(self):
|
||||||
|
# when chained ts2phc ptp0 sync_state is freerun -- overall state would be freerun
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypeGNSS,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Failure_Nofix
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Freerun
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__ts2phc_ptp0_locked__overall_locked(self):
|
||||||
|
# when chained ts2phc ptp0 sync_state is locked -- overall state would be locked
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypeGNSS,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Freerun,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Failure_Nofix
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Locked
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__ts2phc_ptp0_locked_no_ptp4l_for_ptp0__overall_locked(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
# when chained ts2phc ptp0 sync_state is locked -- overall state would be locked
|
||||||
|
# In this case there are no ptp4l instances with ptp0
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptpx"],
|
||||||
|
sync_state=PtpState.Freerun,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Freerun,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Failure_Nofix
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Locked
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__no_source_for_ptp0__overall_freerun(self):
|
||||||
|
# when chained ptp4l ptp0 sync_source NA (neither gnss nor ptp) -- overall
|
||||||
|
# state would be freerun
|
||||||
|
# In this case there are no ts2phc instances with ptp0
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
# In this case, practically ptp0's ptp4l instance sync_state would be
|
||||||
|
# PtpState.Freerun, as there is no sync source. But still using locked state.
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypeNA,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptpx"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Freerun
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__no_backtrack_for_ptp0__overall_freerun(self):
|
||||||
|
# when chained ptp0 is not included neither on ptp4l nor ts2phc -- overall
|
||||||
|
# state would be freerun
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptpx"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptpx"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Freerun
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
||||||
|
|
||||||
|
def test__get_overall_sync_state__os_clock_no_ptp_device__overall_freerun(self):
|
||||||
|
# when there is no sync_source e.g. ptp0 for os_clock -- overall state would be freerun
|
||||||
|
self._setup()
|
||||||
|
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source=None)
|
||||||
|
|
||||||
|
ptp4ldata0 = PTP4lData(
|
||||||
|
ptp_devices=["ptp0"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
ptp4ldata1 = PTP4lData(
|
||||||
|
ptp_devices=["ptp1"],
|
||||||
|
sync_state=PtpState.Locked,
|
||||||
|
sync_source=constants.ClockSourceType.TypePTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
ts2phcdata0 = ts2phcData(
|
||||||
|
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
ts2phcdata1 = ts2phcData(
|
||||||
|
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
|
||||||
|
)
|
||||||
|
|
||||||
|
testdata = TestData(
|
||||||
|
osclock=osclockdata,
|
||||||
|
ptp4l=[ptp4ldata0, ptp4ldata1],
|
||||||
|
ts2phc=[ts2phcdata0, ts2phcdata1],
|
||||||
|
)
|
||||||
|
expected = OverallClockState.Freerun
|
||||||
|
self._test__get_overall_sync_state(testdata, expected)
|
@ -0,0 +1,45 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
import mock
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from trackingfunctionsdk.common.helpers.gnss_monitor import GnssMonitor
|
||||||
|
|
||||||
|
testpath = os.environ.get("TESTPATH", "")
|
||||||
|
|
||||||
|
|
||||||
|
class GnssMonitorTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_check_config_file_interfaces(self):
|
||||||
|
cgu_path = testpath + "test_input_files/mock_cgu_output_logan_beach"
|
||||||
|
gnss_config = testpath + "test_input_files/ts2phc_valid.conf"
|
||||||
|
self.gnssmon = GnssMonitor(gnss_config, cgu_path = cgu_path)
|
||||||
|
self.assertEqual(self.gnssmon._check_config_file_interfaces(), ['ens1f0', 'ens2f0'])
|
||||||
|
|
||||||
|
def test_set_ptp_devices(self):
|
||||||
|
cgu_path = testpath + "test_input_files/mock_cgu_output_logan_beach"
|
||||||
|
gnss_config = testpath + "test_input_files/ts2phc_valid.conf"
|
||||||
|
with mock.patch('trackingfunctionsdk.common.helpers.ptpsync.glob',
|
||||||
|
return_value=[]):
|
||||||
|
self.gnssmon = GnssMonitor(gnss_config, cgu_path = cgu_path)
|
||||||
|
self.assertEqual(self.gnssmon.get_ptp_devices(),[])
|
||||||
|
|
||||||
|
with mock.patch('trackingfunctionsdk.common.helpers.ptpsync.glob',
|
||||||
|
side_effect=[['/hostsys/class/net/ens1f0/device/ptp/ptp0'],
|
||||||
|
['/hostsys/class/net/ens2f0/device/ptp/ptp1']
|
||||||
|
]):
|
||||||
|
self.gnssmon.set_ptp_devices()
|
||||||
|
|
||||||
|
self.assertEqual(set(self.gnssmon.get_ptp_devices()),set(['ptp0','ptp1']))
|
||||||
|
|
||||||
|
with mock.patch('trackingfunctionsdk.common.helpers.ptpsync.glob',
|
||||||
|
side_effect=[['/hostsys/class/net/ens1f0/device/ptp/ptp0'],
|
||||||
|
['/hostsys/class/net/ens2f0/device/ptp/ptp0']
|
||||||
|
]):
|
||||||
|
self.gnssmon.set_ptp_devices()
|
||||||
|
|
||||||
|
self.assertEqual(self.gnssmon.get_ptp_devices(),['ptp0'])
|
@ -0,0 +1,35 @@
|
|||||||
|
[global]
|
||||||
|
##
|
||||||
|
## Default Data Set
|
||||||
|
##
|
||||||
|
boundary_clock_jbod 1
|
||||||
|
clock_servo linreg
|
||||||
|
delay_mechanism E2E
|
||||||
|
domainNumber 24
|
||||||
|
logAnnounceInterval -3
|
||||||
|
logMinDelayReqInterval -4
|
||||||
|
logSyncInterval -4
|
||||||
|
message_tag ptp-inst1
|
||||||
|
network_transport L2
|
||||||
|
summary_interval 6
|
||||||
|
time_stamping hardware
|
||||||
|
tx_timestamp_timeout 700
|
||||||
|
uds_address /var/run/ptp4l-ptp-inst1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[enp81s0f3]
|
||||||
|
##
|
||||||
|
## Associated interface: oam1
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
[enp81s0f4]
|
||||||
|
##
|
||||||
|
|
||||||
|
[unicast_master_table]
|
||||||
|
table_id 1
|
||||||
|
logQueryInterval 2
|
||||||
|
UDPv4 192.168.1.11
|
||||||
|
UDPv4 192.168.2.22
|
||||||
|
UDPv4 192.168.3.33
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2022-2023 Wind River Systems, Inc.
|
# Copyright (c) 2022-2025 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
@ -58,7 +58,7 @@ class OsClockMonitorTests(unittest.TestCase):
|
|||||||
mo.side_effect = handlers
|
mo.side_effect = handlers
|
||||||
self.assertEqual(self.clockmon._get_phc2sys_command_line_option("/var/run/", "-s"), None)
|
self.assertEqual(self.clockmon._get_phc2sys_command_line_option("/var/run/", "-s"), None)
|
||||||
|
|
||||||
@mock.patch('trackingfunctionsdk.common.helpers.os_clock_monitor.glob',
|
@mock.patch('trackingfunctionsdk.common.helpers.ptpsync.glob',
|
||||||
side_effect=[['/hostsys/class/net/ens1f0/device/ptp/ptp0'],
|
side_effect=[['/hostsys/class/net/ens1f0/device/ptp/ptp0'],
|
||||||
['/hostsys/class/net/ens1f0/device/ptp/ptp0',
|
['/hostsys/class/net/ens1f0/device/ptp/ptp0',
|
||||||
'/hostsys/class/net/ens1f0/device/ptp/ptp1'],
|
'/hostsys/class/net/ens1f0/device/ptp/ptp1'],
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
import mock
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from trackingfunctionsdk.common.helpers.ptp_monitor import PtpMonitor
|
||||||
|
|
||||||
|
testpath = os.environ.get("TESTPATH", "")
|
||||||
|
|
||||||
|
holdover_seconds = 15
|
||||||
|
poll_freq_seconds = 15
|
||||||
|
phc2sys_service_name = "phc-inst1"
|
||||||
|
ptp4l_instance = "ptp-inst1"
|
||||||
|
|
||||||
|
|
||||||
|
class PtpMonitorTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_check_config_file_interfaces(self):
|
||||||
|
self.ptpmon = PtpMonitor(
|
||||||
|
ptp4l_instance,
|
||||||
|
holdover_seconds,
|
||||||
|
poll_freq_seconds,
|
||||||
|
phc2sys_service_name,
|
||||||
|
init=False,
|
||||||
|
)
|
||||||
|
self.ptpmon.ptp4l_config = testpath + "test_input_files/ptp4l-ptp-inst1.conf"
|
||||||
|
self.assertEqual(
|
||||||
|
self.ptpmon._check_config_file_interfaces(), ["enp81s0f3", "enp81s0f4"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_ptp_devices(self):
|
||||||
|
self.ptpmon = PtpMonitor(
|
||||||
|
ptp4l_instance,
|
||||||
|
holdover_seconds,
|
||||||
|
poll_freq_seconds,
|
||||||
|
phc2sys_service_name,
|
||||||
|
init=False,
|
||||||
|
)
|
||||||
|
self.ptpmon.ptp4l_config = testpath + "test_input_files/ptp4l-ptp-inst1.conf"
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"trackingfunctionsdk.common.helpers.ptpsync.glob", return_value=[]
|
||||||
|
):
|
||||||
|
self.ptpmon.set_ptp_devices()
|
||||||
|
self.assertEqual(self.ptpmon.get_ptp_devices(), [])
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"trackingfunctionsdk.common.helpers.ptpsync.glob",
|
||||||
|
side_effect=[
|
||||||
|
["/hostsys/class/net/enp81s0f3/device/ptp/ptp0"],
|
||||||
|
["/hostsys/class/net/enp81s0f4/device/ptp/ptp1"],
|
||||||
|
],
|
||||||
|
):
|
||||||
|
self.ptpmon.set_ptp_devices()
|
||||||
|
self.assertEqual(set(self.ptpmon.get_ptp_devices()), set(["ptp0", "ptp1"]))
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"trackingfunctionsdk.common.helpers.ptpsync.glob",
|
||||||
|
side_effect=[
|
||||||
|
["/hostsys/class/net/enp81s0f3/device/ptp/ptp0"],
|
||||||
|
["/hostsys/class/net/enp81s0f4/device/ptp/ptp0"],
|
||||||
|
],
|
||||||
|
):
|
||||||
|
self.ptpmon.set_ptp_devices()
|
||||||
|
|
||||||
|
self.assertEqual(self.ptpmon.get_ptp_devices(), ["ptp0"])
|
6
tox.ini
6
tox.ini
@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = linters,py36
|
envlist = linters,py39
|
||||||
minversion = 2.3
|
minversion = 2.3
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
sitepackages=False
|
sitepackages=False
|
||||||
@ -18,8 +18,8 @@ deps =
|
|||||||
allowlist_externals =
|
allowlist_externals =
|
||||||
bash
|
bash
|
||||||
|
|
||||||
[testenv:py36]
|
[testenv:py39]
|
||||||
basepython = python3.6
|
basepython = python3.9
|
||||||
setenv =
|
setenv =
|
||||||
TESTPATH=./notificationservice-base-v2/docker/ptptrackingfunction/trackingfunctionsdk/tests/
|
TESTPATH=./notificationservice-base-v2/docker/ptptrackingfunction/trackingfunctionsdk/tests/
|
||||||
commands =
|
commands =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user