From d6702a824ee9ef5e9042aa08ae558ba8775c5a67 Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Fri, 17 Jan 2025 16:27:48 +0900 Subject: [PATCH 1/7] test: add sonic_xcvr testing with xcvr-emu Signed-off-by: Wataru Ishida --- Makefile | 13 +++ docker/Dockerfile | 12 +++ docker/pyproject.toml | 10 +++ docker/sonic_py_common/pyproject.toml | 7 ++ .../sonic_py_common/__init__.py | 0 .../sonic_py_common/sonic_py_common/logger.py | 3 + docker/tests/sonic_xcvr_with_emu/test_cmis.py | 82 +++++++++++++++++++ 7 files changed, 127 insertions(+) create mode 100644 Makefile create mode 100644 docker/Dockerfile create mode 100644 docker/pyproject.toml create mode 100644 docker/sonic_py_common/pyproject.toml create mode 100644 docker/sonic_py_common/sonic_py_common/__init__.py create mode 100644 docker/sonic_py_common/sonic_py_common/logger.py create mode 100644 docker/tests/sonic_xcvr_with_emu/test_cmis.py diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..6fe1d7eef --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +DOCKER_CMD ?= docker +SONIC_XCVR_IMAGE ?= sonic-xcvr + +.PHONY: test_sonic_xcvr_with_emu +test_sonic_xcvr_with_emu: + $(DOCKER_CMD) build -f docker/Dockerfile -t $(SONIC_XCVR_IMAGE) . + $(DOCKER_CMD) run -it \ + -v `pwd`/docker/pyproject.toml:/sonic_platform_base/pyproject.toml \ + -v `pwd`/sonic_platform_base:/sonic_platform_base/sonic_platform_base \ + -v `pwd`/docker/tests:/sonic_platform_base/tests \ + -w /sonic_platform_base \ + $(SONIC_XCVR_IMAGE) \ + python -m pytest -v . diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..8d0a003f5 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/ishidawataru/xcvr-emu:main + +WORKDIR /sonic_platform_base + +RUN --mount=type=bind,source=docker/sonic_py_common,target=/sonic_py_common,rw \ + cd /sonic_py_common && pip install . + +RUN mkdir sonic_platform_base && touch sonic_platform_base/__init__.py + +COPY docker/pyproject.toml . + +RUN pip install '.[dev]' diff --git a/docker/pyproject.toml b/docker/pyproject.toml new file mode 100644 index 000000000..907957499 --- /dev/null +++ b/docker/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "sonic-platform-common" +version = "1.0.0" + +[project.optional-dependencies] +dev = ["pytest", "mock", "PyYAML", "pytest-asyncio"] diff --git a/docker/sonic_py_common/pyproject.toml b/docker/sonic_py_common/pyproject.toml new file mode 100644 index 000000000..e14ff5eab --- /dev/null +++ b/docker/sonic_py_common/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "sonic-py-common" +version = "1.0.0" diff --git a/docker/sonic_py_common/sonic_py_common/__init__.py b/docker/sonic_py_common/sonic_py_common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docker/sonic_py_common/sonic_py_common/logger.py b/docker/sonic_py_common/sonic_py_common/logger.py new file mode 100644 index 000000000..384955e91 --- /dev/null +++ b/docker/sonic_py_common/sonic_py_common/logger.py @@ -0,0 +1,3 @@ +from unittest.mock import MagicMock + +Logger = MagicMock() diff --git a/docker/tests/sonic_xcvr_with_emu/test_cmis.py b/docker/tests/sonic_xcvr_with_emu/test_cmis.py new file mode 100644 index 000000000..2976b032e --- /dev/null +++ b/docker/tests/sonic_xcvr_with_emu/test_cmis.py @@ -0,0 +1,82 @@ +import pytest + +from sonic_platform_base.sonic_xcvr.xcvr_eeprom import XcvrEeprom +from sonic_platform_base.sonic_xcvr.api.public.cmis import CmisApi +from sonic_platform_base.sonic_xcvr.mem_maps.public.cmis import CmisMemMap +from sonic_platform_base.sonic_xcvr.codes.public.cmis import CmisCodes + +from xcvr_emu.transceiver import CMISTransceiver # type: ignore +from xcvr_emu.proto.emulator_pb2 import ( # type: ignore + ReadRequest, + WriteRequest, +) + + +class XcvrEmuEeprom(XcvrEeprom): + def __init__(self, config: dict): + + codes = CmisCodes + mem_map = CmisMemMap(codes) + + self.xcvr = CMISTransceiver( + 0, + { + "present": True, + "defaults": config, + }, + ) + + super().__init__(self._read, self._write, mem_map) + + def _read(self, offset, num_bytes): + if not self.xcvr.present: + return None + # convert optoe offset to SFF page and offset + # optoe maps the SFF 2D address to a linear address + page = offset // 128 + if page > 0: + page = page - 1 + + if offset > 128: + offset = (offset % 128) + 128 + + return self.xcvr.read( + ReadRequest(index=0, offset=offset, page=page, length=num_bytes) + ) + + def _write(self, offset, num_bytes, write_buffer): + assert len(write_buffer) <= num_bytes + # convert optoe offset to SFF page and offset + # optoe maps the SFF 2D address to a linear address + page = offset // 128 + if page > 0: + page = page - 1 + + if offset > 128: + offset = (offset % 128) + 128 + + return self.xcvr.write( + WriteRequest( + index=0, + page=page, + offset=offset, + length=num_bytes, + data=bytes(write_buffer), + ) + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "emu_response, expected", [("1234567890", "1234567890"), ("ABCD", "ABCD")] +) +async def test_get_model(emu_response, expected): + eeprom = XcvrEmuEeprom( + { + "VendorPN": emu_response, + }, + ) + api = CmisApi(eeprom) + result = api.get_model() + await eeprom.xcvr.plugout() + assert result == expected From a6e53b2e0436a5ed36b2b4c5bbbbfc20b11abbc3 Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Fri, 17 Jan 2025 16:37:22 +0900 Subject: [PATCH 2/7] fix: remove trailing null characters from decoded string in StringRegField --- sonic_platform_base/sonic_xcvr/fields/xcvr_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py b/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py index 1b7d3c856..a9f9f5e27 100644 --- a/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py +++ b/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py @@ -221,7 +221,7 @@ def __init__(self, name, offset, *fields, **kwargs): self.format = kwargs.get("format", ">%ds" % self.size) def decode(self, raw_data, **decoded_deps): - return struct.unpack(self.format, raw_data)[0].decode(self.encoding, 'ignore') + return struct.unpack(self.format, raw_data)[0].decode(self.encoding, 'ignore').rstrip("\x00") class CodeRegField(RegField): """ From a8cee0c18afbca739141cecc5af6e3eb86ddf29a Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Fri, 17 Jan 2025 16:43:11 +0900 Subject: [PATCH 3/7] ci: run make test_sonic_xcvr_with_emu Signed-off-by: Wataru Ishida --- .../workflows/test_sonic_xcvr_with_emu.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/test_sonic_xcvr_with_emu.yml diff --git a/.github/workflows/test_sonic_xcvr_with_emu.yml b/.github/workflows/test_sonic_xcvr_with_emu.yml new file mode 100644 index 000000000..578ee6fad --- /dev/null +++ b/.github/workflows/test_sonic_xcvr_with_emu.yml @@ -0,0 +1,20 @@ +name: Test sonic-xcvr with xcvr-emu + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: test sonic-xcvr with xcvr-emu + run: make test_sonic_xcvr_with_emu From cd938ccd6d0593697f4927702c3ebbd46dba95df Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Fri, 17 Jan 2025 17:02:58 +0900 Subject: [PATCH 4/7] ci: ignore docker directory in azure-pipeline --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a4cb4dbe3..34c26b143 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -77,7 +77,7 @@ steps: set -ex pip3 install ".[testing]" pip3 uninstall --yes sonic-platform-common - pytest + pytest --ignore docker displayName: 'Test Python 3' - task: PublishTestResults@2 From bc2b7297513ae16777434b0db94e83d4c97c1c12 Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Fri, 17 Jan 2025 17:12:12 +0900 Subject: [PATCH 5/7] Revert "fix: remove trailing null characters from decoded string in StringRegField" This reverts commit df1852df9bd9dce295008b154ee132125dc7b92c. It seems leaving the traling null characters is intentional. --- sonic_platform_base/sonic_xcvr/fields/xcvr_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py b/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py index a9f9f5e27..1b7d3c856 100644 --- a/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py +++ b/sonic_platform_base/sonic_xcvr/fields/xcvr_field.py @@ -221,7 +221,7 @@ def __init__(self, name, offset, *fields, **kwargs): self.format = kwargs.get("format", ">%ds" % self.size) def decode(self, raw_data, **decoded_deps): - return struct.unpack(self.format, raw_data)[0].decode(self.encoding, 'ignore').rstrip("\x00") + return struct.unpack(self.format, raw_data)[0].decode(self.encoding, 'ignore') class CodeRegField(RegField): """ From 6fdffa2f052e806971a8c47e2afd9e0e57eeb195 Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Fri, 17 Jan 2025 17:14:01 +0900 Subject: [PATCH 6/7] fix: remove trailing null characters in the test code --- docker/tests/sonic_xcvr_with_emu/test_cmis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/tests/sonic_xcvr_with_emu/test_cmis.py b/docker/tests/sonic_xcvr_with_emu/test_cmis.py index 2976b032e..c569e4ab0 100644 --- a/docker/tests/sonic_xcvr_with_emu/test_cmis.py +++ b/docker/tests/sonic_xcvr_with_emu/test_cmis.py @@ -78,5 +78,6 @@ async def test_get_model(emu_response, expected): ) api = CmisApi(eeprom) result = api.get_model() + result = result.rstrip("\x00") await eeprom.xcvr.plugout() assert result == expected From c93d305bcfec02bb093fdc8c89a46a2d724b939c Mon Sep 17 00:00:00 2001 From: Wataru Ishida Date: Tue, 8 Apr 2025 16:47:13 +0900 Subject: [PATCH 7/7] test: test get_media_lane_count() with eeprom dump from Passive Copper ELB modules Signed-off-by: Wataru Ishida --- Makefile | 2 +- docker/Dockerfile | 2 +- docker/tests/sonic_xcvr_with_emu/test_cmis.py | 55 ++++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 6fe1d7eef..d7f7c6753 100644 --- a/Makefile +++ b/Makefile @@ -10,4 +10,4 @@ test_sonic_xcvr_with_emu: -v `pwd`/docker/tests:/sonic_platform_base/tests \ -w /sonic_platform_base \ $(SONIC_XCVR_IMAGE) \ - python -m pytest -v . + python -m pytest -v --log-cli-level=INFO . diff --git a/docker/Dockerfile b/docker/Dockerfile index 8d0a003f5..b4eb8fde5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/ishidawataru/xcvr-emu:main +FROM ghcr.io/ishidawataru/xcvr-emu:latest WORKDIR /sonic_platform_base diff --git a/docker/tests/sonic_xcvr_with_emu/test_cmis.py b/docker/tests/sonic_xcvr_with_emu/test_cmis.py index c569e4ab0..b48a79479 100644 --- a/docker/tests/sonic_xcvr_with_emu/test_cmis.py +++ b/docker/tests/sonic_xcvr_with_emu/test_cmis.py @@ -1,4 +1,5 @@ import pytest +import logging from sonic_platform_base.sonic_xcvr.xcvr_eeprom import XcvrEeprom from sonic_platform_base.sonic_xcvr.api.public.cmis import CmisApi @@ -10,13 +11,17 @@ ReadRequest, WriteRequest, ) +from cmis import MemMap +from cmis.optoe import EEPROM + +logger = logging.getLogger(__name__) + class XcvrEmuEeprom(XcvrEeprom): - def __init__(self, config: dict): + def __init__(self, config: dict, mem_map=None): codes = CmisCodes - mem_map = CmisMemMap(codes) self.xcvr = CMISTransceiver( 0, @@ -24,9 +29,10 @@ def __init__(self, config: dict): "present": True, "defaults": config, }, + mem_map, ) - super().__init__(self._read, self._write, mem_map) + super().__init__(self._read, self._write, CmisMemMap(codes)) def _read(self, offset, num_bytes): if not self.xcvr.present: @@ -81,3 +87,46 @@ async def test_get_model(emu_response, expected): result = result.rstrip("\x00") await eeprom.xcvr.plugout() assert result == expected + +# hexdump taken from https://github.com/sonic-net/sonic-platform-common/issues/489#issue-2445591891 +EEPROM_HEXDUMP = """00000000 18 40 00 07 00 00 00 00 00 00 00 00 00 00 17 00 |.@..............| +00000010 82 00 00 00 00 00 00 00 17 80 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 |................| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 00 00 11 00 88 00 00 00 00 00 00 00 00 00 00 00 |................| +00000080 18 43 49 53 43 4f 20 20 20 20 20 20 20 20 20 20 |.CISCO | +00000090 20 00 06 f6 36 38 2d 31 30 33 32 30 35 2d 30 32 | ...68-103205-02| +000000a0 20 20 20 20 32 20 46 41 42 32 36 31 31 30 30 43 | 2 FAB261100C| +000000b0 51 20 20 20 20 20 32 32 31 30 31 38 20 20 00 00 |Q 221018 ..| +000000c0 00 00 00 00 00 00 00 00 e0 78 00 00 00 00 00 00 |.........x......| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f9 00 |................| +000000e0 1b 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|""" + +@pytest.mark.asyncio +async def test_get_application_advertisement(): + e = EEPROM() + e.load(EEPROM_HEXDUMP) + m = MemMap(e) + eeprom = XcvrEmuEeprom({}, m) + api = CmisApi(eeprom) + + data = e.read(0, 0, 0, 256) + for f, v in m.decode(0, 0, data): + logger.info(f.to_str(value=v)) + + result = api.get_application_advertisement() + logger.info(f"Application Advertisement: {result}") + + app_id = list(result.keys())[0] + + with pytest.raises(KeyError): + result = api.get_media_lane_count() + + result = api.get_media_lane_count(app_id) + logger.info(f"Media Lane Count: {result}") + + await eeprom.xcvr.plugout()