From 366b9ecdbe135b46b52efeb61dff6b6b96cf8886 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 14:40:14 -0500 Subject: [PATCH 01/12] I just like well-formatted files and I can't tell a lie Format schema files with "python3 -m json.tool" --- schemas/Connection.json | 9 +++++++-- schemas/Link.json | 8 ++++++-- schemas/Location.json | 2 +- schemas/Node.json | 9 +++++++-- schemas/Path.json | 8 ++++++-- schemas/Port.json | 7 ++++++- schemas/Service.json | 2 +- schemas/Topology.json | 12 ++++++++++-- 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/schemas/Connection.json b/schemas/Connection.json index 2644d0c..8204083 100644 --- a/schemas/Connection.json +++ b/schemas/Connection.json @@ -114,5 +114,10 @@ ] } }, - "required": ["id", "name", "ingress_port", "egress_port"] -} \ No newline at end of file + "required": [ + "id", + "name", + "ingress_port", + "egress_port" + ] +} diff --git a/schemas/Link.json b/schemas/Link.json index 725e95a..bc37f8c 100644 --- a/schemas/Link.json +++ b/schemas/Link.json @@ -88,5 +88,9 @@ } } }, - "required": ["id", "name", "ports"] -} \ No newline at end of file + "required": [ + "id", + "name", + "ports" + ] +} diff --git a/schemas/Location.json b/schemas/Location.json index afbdfff..5ed325d 100644 --- a/schemas/Location.json +++ b/schemas/Location.json @@ -18,4 +18,4 @@ } }, "required": [] -} \ No newline at end of file +} diff --git a/schemas/Node.json b/schemas/Node.json index 26acd2c..ad4d4b8 100644 --- a/schemas/Node.json +++ b/schemas/Node.json @@ -24,5 +24,10 @@ ] } }, - "required": ["id", "location", "name", "ports"] -} \ No newline at end of file + "required": [ + "id", + "location", + "name", + "ports" + ] +} diff --git a/schemas/Path.json b/schemas/Path.json index e97e509..2d00f29 100644 --- a/schemas/Path.json +++ b/schemas/Path.json @@ -48,5 +48,9 @@ ] } }, - "required": ["id", "name", "links"] -} \ No newline at end of file + "required": [ + "id", + "name", + "links" + ] +} diff --git a/schemas/Port.json b/schemas/Port.json index 7f3f733..1f7a2ce 100644 --- a/schemas/Port.json +++ b/schemas/Port.json @@ -94,5 +94,10 @@ } } }, - "required": ["id", "name", "node", "status"] + "required": [ + "id", + "name", + "node", + "status" + ] } diff --git a/schemas/Service.json b/schemas/Service.json index 2f18041..823686b 100644 --- a/schemas/Service.json +++ b/schemas/Service.json @@ -35,4 +35,4 @@ } }, "required": [] -} \ No newline at end of file +} diff --git a/schemas/Topology.json b/schemas/Topology.json index bf4603c..7eb57a6 100644 --- a/schemas/Topology.json +++ b/schemas/Topology.json @@ -45,5 +45,13 @@ ] } }, - "required": ["id", "name", "version","model_version","time_stamp","nodes", "links"] -} \ No newline at end of file + "required": [ + "id", + "name", + "version", + "model_version", + "time_stamp", + "nodes", + "links" + ] +} From b5c1ecb5acded1979210cda2779f625f6efe9562 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 15:14:44 -0500 Subject: [PATCH 02/12] Add some JSON schema validation tests --- tests/test_schemas.py | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_schemas.py diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000..399bd44 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,83 @@ +import json +import unittest +from pathlib import Path + +import jsonschema + +from . import TestData + + +class JSONSchemaTests(unittest.TestCase): + """ + Test data files against corresponding JSON schema. + """ + + SCHEMA_DIR = Path(__file__).parent.parent / "schemas" + + CONNECTION_SCHEMA_FILE = SCHEMA_DIR / "Connection.json" + LINK_SCHEMA_FILE = SCHEMA_DIR / "Link.json" + LOCATION_SCHEMA_FILE = SCHEMA_DIR / "Location.json" + NODE_SCHEMA_FILE = SCHEMA_DIR / "Node.json" + PATH_SCHEMA_FILE = SCHEMA_DIR / "Path.json" + PORT_SCHEMA_FILE = SCHEMA_DIR / "Port.json" + SERVICE_SCHEMA_FILE = SCHEMA_DIR / "Service.json" + TOPOLOGY_SCHEMA_FILE = SCHEMA_DIR / "Topology.json" + + def _read_json(self, path: Path): + """ + Read JSON and return a dict. + """ + return json.loads(path.read_text()) + + def test_connection_request_schema(self): + jsonschema.validate( + self._read_json(TestData.CONNECTION_FILE_REQ), + self._read_json(self.CONNECTION_SCHEMA_FILE), + ) + + def test_link_schema(self): + jsonschema.validate( + self._read_json(TestData.LINK_FILE), + self._read_json(self.LINK_SCHEMA_FILE), + ) + + def test_location_schema(self): + jsonschema.validate( + self._read_json(TestData.LOCATION_FILE), + self._read_json(self.LOCATION_SCHEMA_FILE), + ) + + def test_node_schema(self): + jsonschema.validate( + self._read_json(TestData.NODE_FILE), + self._read_json(self.NODE_SCHEMA_FILE), + ) + + @unittest.skip(reason="Path files are not implemented.") + def test_path_schema(self): + jsonschema.validate( + self._read_json(TestData.PATH_FILE), + self._read_json(self.PATH_SCHEMA_FILE), + ) + + def test_port_schema(self): + jsonschema.validate( + self._read_json(TestData.PORT_FILE), + self._read_json(self.PORT_SCHEMA_FILE), + ) + + def test_service_schema(self): + jsonschema.validate( + self._read_json(TestData.SERVICE_FILE), + self._read_json(self.SERVICE_SCHEMA_FILE), + ) + + def test_topology_schema(self): + jsonschema.validate( + self._read_json(TestData.TOPOLOGY_FILE_AMLIGHT), + self._read_json(self.TOPOLOGY_SCHEMA_FILE), + ) + + +if __name__ == "__main__": + unittest.main() From b96b8260700bbcc73f3f1a9bd261ca93f0a83aa0 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 15:19:25 -0500 Subject: [PATCH 03/12] Add jsonschema as a test dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 9235f1d..da1a1aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ Issues = "https://github.com/atlanticwave-sdx/datamodel/issues" test = [ "pytest >= 7.1.2", "pytest-cov >= 3.0.0", + "jsonschema >= 4.19.0", ] [options.packages.find] From f60e655ac84eed73ba1cfd60b8b0c31c28da4ee2 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 20:37:15 -0500 Subject: [PATCH 04/12] Use jsonref to fully expand $ref in schemas --- pyproject.toml | 1 + tests/test_schemas.py | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da1a1aa..88841b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ test = [ "pytest >= 7.1.2", "pytest-cov >= 3.0.0", "jsonschema >= 4.19.0", + "jsonref >= 1.1.0", ] [options.packages.find] diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 399bd44..231c8f1 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -2,6 +2,7 @@ import unittest from pathlib import Path +import jsonref import jsonschema from . import TestData @@ -29,53 +30,61 @@ def _read_json(self, path: Path): """ return json.loads(path.read_text()) + def _read_schema(self, path: Path): + """ + Read schema and return a dict with expanded $ref. + """ + return jsonref.loads( + path.read_text(), base_uri=path.absolute().as_uri() + ) + def test_connection_request_schema(self): jsonschema.validate( self._read_json(TestData.CONNECTION_FILE_REQ), - self._read_json(self.CONNECTION_SCHEMA_FILE), + self._read_schema(self.CONNECTION_SCHEMA_FILE), ) def test_link_schema(self): jsonschema.validate( self._read_json(TestData.LINK_FILE), - self._read_json(self.LINK_SCHEMA_FILE), + self._read_schema(self.LINK_SCHEMA_FILE), ) def test_location_schema(self): jsonschema.validate( self._read_json(TestData.LOCATION_FILE), - self._read_json(self.LOCATION_SCHEMA_FILE), + self._read_schema(self.LOCATION_SCHEMA_FILE), ) def test_node_schema(self): jsonschema.validate( self._read_json(TestData.NODE_FILE), - self._read_json(self.NODE_SCHEMA_FILE), + self._read_schema(self.NODE_SCHEMA_FILE), ) @unittest.skip(reason="Path files are not implemented.") def test_path_schema(self): jsonschema.validate( self._read_json(TestData.PATH_FILE), - self._read_json(self.PATH_SCHEMA_FILE), + self._read_schema(self.PATH_SCHEMA_FILE), ) def test_port_schema(self): jsonschema.validate( self._read_json(TestData.PORT_FILE), - self._read_json(self.PORT_SCHEMA_FILE), + self._read_schema(self.PORT_SCHEMA_FILE), ) def test_service_schema(self): jsonschema.validate( self._read_json(TestData.SERVICE_FILE), - self._read_json(self.SERVICE_SCHEMA_FILE), + self._read_schema(self.SERVICE_SCHEMA_FILE), ) def test_topology_schema(self): jsonschema.validate( self._read_json(TestData.TOPOLOGY_FILE_AMLIGHT), - self._read_json(self.TOPOLOGY_SCHEMA_FILE), + self._read_schema(self.TOPOLOGY_SCHEMA_FILE), ) From 96f88454258e2754680df40a151a410ff4c84ba4 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 20:43:56 -0500 Subject: [PATCH 05/12] Use $ref to external files correctly --- schemas/Connection.json | 10 +++++----- schemas/Link.json | 2 +- schemas/Node.json | 4 ++-- schemas/Path.json | 2 +- schemas/Topology.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/schemas/Connection.json b/schemas/Connection.json index 8204083..5804f47 100644 --- a/schemas/Connection.json +++ b/schemas/Connection.json @@ -15,10 +15,10 @@ "maximum": 7 }, "ingress_port": { - "$ref": "Port" + "$ref": "Port.json" }, "egress_port": { - "$ref": "Port" + "$ref": "Port.json" }, "end_time": { "type": "string", @@ -47,14 +47,14 @@ "type": "array", "items": [ { - "$ref": "Link" + "$ref": "Link.json" } ] }, "inclusive_links": { "type": "array", "items": { - "$ref": "Link" + "$ref": "Link.json" } }, "bandwidth_required": { @@ -101,7 +101,7 @@ "type": "array", "items": [ { - "$ref": "Path" + "$ref": "Path.json" } ] }, diff --git a/schemas/Link.json b/schemas/Link.json index bc37f8c..f2f54f9 100644 --- a/schemas/Link.json +++ b/schemas/Link.json @@ -19,7 +19,7 @@ "uniqueItems": true, "items": [ { - "$ref": "Port" + "$ref": "Port.json" } ] }, diff --git a/schemas/Node.json b/schemas/Node.json index ad4d4b8..cac0bf5 100644 --- a/schemas/Node.json +++ b/schemas/Node.json @@ -13,13 +13,13 @@ "type": "string" }, "location": { - "$ref": "location.json" + "$ref": "Location.json" }, "ports": { "type": "array", "items": [ { - "$ref": "port.json" + "$ref": "Port.json" } ] } diff --git a/schemas/Path.json b/schemas/Path.json index 2d00f29..659d88d 100644 --- a/schemas/Path.json +++ b/schemas/Path.json @@ -43,7 +43,7 @@ "type": "array", "items": [ { - "$ref": "Link" + "$ref": "Link.json" } ] } diff --git a/schemas/Topology.json b/schemas/Topology.json index 7eb57a6..a22a48d 100644 --- a/schemas/Topology.json +++ b/schemas/Topology.json @@ -26,13 +26,13 @@ "minimum": 0 }, "domain_service": { - "$ref": "Service" + "$ref": "Service.json" }, "nodes": { "type": "array", "items": [ { - "$ref": "Node" + "$ref": "Node.json" } ] }, @@ -40,7 +40,7 @@ "type": "array", "items": [ { - "$ref": "Link" + "$ref": "Link.json" } ] } From 35f871c3a3a40843c2a93fe107837251f66fe46d Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 20:50:37 -0500 Subject: [PATCH 06/12] Port label ranges are arrays of strings, not arrays of arrays --- schemas/Port.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/Port.json b/schemas/Port.json index 1f7a2ce..75f127f 100644 --- a/schemas/Port.json +++ b/schemas/Port.json @@ -57,7 +57,7 @@ "label_range": { "type": "array", "items": { - "type": "array", + "type": "string", "minItems": 1, "maxItems": 4096, "uniqueItems": true, From c34f21ccdf744d21e98ca6e7f934370d90c628a3 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 20:51:49 -0500 Subject: [PATCH 07/12] Validate sax and zaoxi topologies as well --- tests/test_schemas.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 231c8f1..8d2b86d 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -81,12 +81,23 @@ def test_service_schema(self): self._read_schema(self.SERVICE_SCHEMA_FILE), ) - def test_topology_schema(self): + def test_topology_schema_amlight(self): jsonschema.validate( self._read_json(TestData.TOPOLOGY_FILE_AMLIGHT), self._read_schema(self.TOPOLOGY_SCHEMA_FILE), ) + def test_topology_schema_sax(self): + jsonschema.validate( + self._read_json(TestData.TOPOLOGY_FILE_SAX), + self._read_schema(self.TOPOLOGY_SCHEMA_FILE), + ) + + def test_topology_schema_zaoxi(self): + jsonschema.validate( + self._read_json(TestData.TOPOLOGY_FILE_ZAOXI), + self._read_schema(self.TOPOLOGY_SCHEMA_FILE), + ) if __name__ == "__main__": unittest.main() From 3a2534c24d373904724a97640be7c846585ad61d Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 21:21:42 -0500 Subject: [PATCH 08/12] Add a note about Path schema --- tests/test_schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 8d2b86d..3b53e1d 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -64,6 +64,8 @@ def test_node_schema(self): @unittest.skip(reason="Path files are not implemented.") def test_path_schema(self): + # We do have a Path schema, but we do not have any actual Path + # data blobs. jsonschema.validate( self._read_json(TestData.PATH_FILE), self._read_schema(self.PATH_SCHEMA_FILE), From e26f98908017fd529481c599b43e676c8bad00ec Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 21:37:53 -0500 Subject: [PATCH 09/12] Use hopefully correct syntax to avoid some vendors --- schemas/Service.json | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/schemas/Service.json b/schemas/Service.json index 823686b..43af522 100644 --- a/schemas/Service.json +++ b/schemas/Service.json @@ -13,14 +13,16 @@ "type": "string" }, "vendor": { - "not": [ - { - "type": "array", - "items": { - "type": "string" + "not": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } } - } - ] + ] + } }, "monitoring_capability": { "anyOf": [ From d7f1ed4d05841179645388c0235486b3f014098f Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 21:40:50 -0500 Subject: [PATCH 10/12] Add a note about the skipped test --- tests/test_schemas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 3b53e1d..6f79b68 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -65,7 +65,8 @@ def test_node_schema(self): @unittest.skip(reason="Path files are not implemented.") def test_path_schema(self): # We do have a Path schema, but we do not have any actual Path - # data blobs. + # data blobs. This test is just to remind ourselves of that + # fact. jsonschema.validate( self._read_json(TestData.PATH_FILE), self._read_schema(self.PATH_SCHEMA_FILE), @@ -101,5 +102,6 @@ def test_topology_schema_zaoxi(self): self._read_schema(self.TOPOLOGY_SCHEMA_FILE), ) + if __name__ == "__main__": unittest.main() From 4f187c6fbf82cd6f6f5925585476ecc6a7433e65 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 17 Aug 2023 21:49:50 -0500 Subject: [PATCH 11/12] Update notes about schemas --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 52dc87c..9e6e814 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,12 @@ supposed to generated and pass on to the SDX controller. ## Topology description schemas -There are defined in the `schema` subfolder. Some attributes of each -objects are requied (Can be found in the API definition) while some -are optional. Two attributes are worth of mentioning: +There are some JSON schemas defined in the [schemas] subfolder. These +are meant to guide and validate model development. + +Some attributes of each objects are requied (can be found in the API +definition) while some are optional. Two attributes are worth +mentioning: 1. In the `service` object, there is a `vendor` attribute for the domain to list device vendors that are NOT in its domain. @@ -74,6 +77,14 @@ domain to list device vendors that are NOT in its domain. 2. In topology, link, node, and port objects, there is a `private` attibute for the domain to list attributes that need to kept private. +There are some unit tests to validate JSON blobs we have against their +corresponding schemas. Another nifty tool is [check-jsonschema]: + +```console +$ check-jsonschema --schemafile schemas/Topology.json \ + src/sdx_datamodel/data/topologies/amlight.json +``` + ## Developing the library @@ -118,3 +129,8 @@ $ python -m unittest -v tests.test_topology_validator [datamodel-cov-badge]: https://coveralls.io/repos/github/atlanticwave-sdx/datamodel/badge.svg?branch=main (Coverage Status) [datamodel-cov]: https://coveralls.io/github/atlanticwave-sdx/datamodel?branch=main + +[check-jsonschema]: https://pypi.org/project/check-jsonschema/ + +[schemas]: ./schemas + From 0a372dfdce1f3f03a48e5992d7e625a9391ba37d Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 18 Aug 2023 09:14:44 -0500 Subject: [PATCH 12/12] Add some negative tests too --- tests/__init__.py | 1 + tests/test_schemas.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index 1f955e6..ce64441 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -16,6 +16,7 @@ class TestData: TOPOLOGY_FILE_AMLIGHT = TOPOLOGY_DIR / "amlight.json" TOPOLOGY_FILE_AMPATH = TOPOLOGY_DIR / "ampath.json" TOPOLOGY_FILE_SAX = TOPOLOGY_DIR / "sax.json" + TOPOLOGY_FILE_SDX = TOPOLOGY_DIR / "sdx.json" TOPOLOGY_FILE_ZAOXI = TOPOLOGY_DIR / "zaoxi.json" REQUESTS_DIR = PACKAGE_DATA_DIR / "requests" diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 6f79b68..85556fa 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -90,12 +90,33 @@ def test_topology_schema_amlight(self): self._read_schema(self.TOPOLOGY_SCHEMA_FILE), ) + def test_topology_schema_ampath(self): + # AmPath is not up-to-date and should fail validation. It is + # missing time stamp and maybe other things. + self.assertRaises( + jsonschema.exceptions.ValidationError, + jsonschema.validate, + self._read_json(TestData.TOPOLOGY_FILE_AMPATH), + self._read_schema(self.TOPOLOGY_SCHEMA_FILE), + ) + def test_topology_schema_sax(self): jsonschema.validate( self._read_json(TestData.TOPOLOGY_FILE_SAX), self._read_schema(self.TOPOLOGY_SCHEMA_FILE), ) + def test_topology_schema_sdx(self): + # SDX topology is a "combined" super-topology of AmLight, SAX, + # and Zaoxi. It is missing the `model_version` required + # property and maybe other things too. + self.assertRaises( + jsonschema.exceptions.ValidationError, + jsonschema.validate, + self._read_json(TestData.TOPOLOGY_FILE_SDX), + self._read_schema(self.TOPOLOGY_SCHEMA_FILE), + ) + def test_topology_schema_zaoxi(self): jsonschema.validate( self._read_json(TestData.TOPOLOGY_FILE_ZAOXI),