From d1deac0ae7854285390df56f9033d44adc89441e Mon Sep 17 00:00:00 2001 From: ajbalogh Date: Wed, 6 Jan 2021 14:25:43 -0800 Subject: [PATCH] align with models 0.1.2 --- README.md | 8 ++-- VERSION | 2 +- snappi/snappicommon.py | 48 +++++++++++++++++-- snappi/snappigenerator.py | 44 ++++++++++++----- snappi/tests/conftest.py | 13 +++++ snappi/tests/test_e2e_port_flow_config.py | 47 ++++++++++++++++++ snappi/tests/test_port_metrics.py | 13 +++++ .../tests/test_snappi_list_serialization.py | 15 ++++++ ...py => test_snappi_object_serialization.py} | 9 +--- 9 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 snappi/tests/test_e2e_port_flow_config.py create mode 100644 snappi/tests/test_port_metrics.py create mode 100644 snappi/tests/test_snappi_list_serialization.py rename snappi/tests/{test_serdes.py => test_snappi_object_serialization.py} (69%) diff --git a/README.md b/README.md index e42158b4..92d930ee 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# ***snappi*** +# snappi Create test case scripts once using the `snappi` client and run them using a traffic generator that conforms to the [Open Traffic Generator API](https://github.com/open-traffic-generator/models/releases). -## *Install the client* +## Install the client ``` pip install snappi ``` -## *Start scripting* +## Start scripting ```python """A simple test that demonstrates the following: - A port that transmits an ethernet/vlan/ipv4/tcp flow @@ -31,7 +31,7 @@ flow.rate.pps = 1000 flow.duration.fixed_packets.packets = 10000 api.set_config(config) -tx_state = api.transmitstate(state='start') +tx_state = api.transmit_state(state='start') api.set_transmit(tx_state) while True: diff --git a/VERSION b/VERSION index 6da28dde..8294c184 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.1 \ No newline at end of file +0.1.2 \ No newline at end of file diff --git a/snappi/snappicommon.py b/snappi/snappicommon.py index dc348391..ab514c45 100644 --- a/snappi/snappicommon.py +++ b/snappi/snappicommon.py @@ -13,7 +13,7 @@ def __init__(self): self.base_url = 'http://127.0.0.1:80' self._session = requests.Session() - def send_recv(self, method, relative_url, payload=None): + def send_recv(self, method, relative_url, payload=None, return_object=None): url = '%s%s' % (self.base_url, relative_url) data = None if payload is not None: @@ -26,7 +26,7 @@ def send_recv(self, method, relative_url, payload=None): if response.ok is not True: raise Exception(response.status_code) elif response.headers['content-type'] == 'application/json': - return yaml.safe_load(response.text) + return return_object.deserialize(yaml.safe_load(response.text)) else: return None @@ -83,7 +83,7 @@ def deserialize(self, encoded_snappi_object): Args ---- - encoded_snappi_object (Union[str, dict]): The object to deserialize. - The supported encodings of obj are json, yaml and python dict. + The supported encodings of str are json and yaml. Returns ------- @@ -133,6 +133,10 @@ def __str__(self): class SnappiList(object): + JSON = 'json' + YAML = 'yaml' + DICT = 'dict' + """Container class for SnappiObject Inheriting classes contain 0..n instances of an OpenAPI components/schemas @@ -201,8 +205,46 @@ def clear(self): self._items.clear() self._index = -1 + def serialize(self, encoding=JSON): + if encoding == SnappiObject.JSON: + return json.dumps(self._encode(), indent=2) + elif encoding == SnappiObject.YAML: + return yaml.safe_dump(self._encode()) + elif encoding == SnappiObject.DICT: + return self._encode() + else: + raise NotImplementedError('Encoding %s not supported' % encoding) + def _encode(self): return [item._encode() for item in self._items] + def deserialize(self, encoded_snappi_list): + """Deserialize a python list into the current snappi list. + + If the input `encoded_snappi_list` does not match the current + snappi list an exception will be raised. + + Args + ---- + - encoded_snappi_list (Union[str, list]): The object to deserialize. + The supported encodings of str are json and yaml. + + Returns + ------- + - obj(snappicommon.SnappiList): A snappi list object + """ + if isinstance(encoded_snappi_list, (str, unicode)): + encoded_snappi_list = yaml.safe_load(encoded_snappi_list) + self._decode(encoded_snappi_list) + return self + + def _decode(self, encoded_snappi_list): + item_class_path = self.__class__.__module__.replace('list', '') + item_class_name = self.__class__.__name__.replace('List', '') + module = importlib.import_module(item_class_path) + object_class = getattr(module, item_class_name) + for item in encoded_snappi_list: + self._add(object_class()._decode(item)) + def __str__(self): return yaml.safe_dump(self._encode()) \ No newline at end of file diff --git a/snappi/snappigenerator.py b/snappi/snappigenerator.py index 89e95287..18e720b1 100644 --- a/snappi/snappigenerator.py +++ b/snappi/snappigenerator.py @@ -94,7 +94,7 @@ def _get_openapi_file(self): self._openapi = yaml.safe_load(openapi_content) def _generate(self): - self._write_paths() + self._write_api_class() self._write_init() return self @@ -103,7 +103,7 @@ def _write_init(self): with open(filename, 'w') as self._fid: self._write(0, 'from .api import Api') - def _write_paths(self): + def _write_api_class(self): api_filename = os.path.join(self._src_dir, 'api.py') with open(api_filename, 'a') as self._fid: self._write(0, 'from .snappicommon import SnappiRestTransport') @@ -130,6 +130,12 @@ def _write_paths(self): else: content = ', content' payload = ', payload=content' + response = parse('$..responses..schema').find(operation) + response_object = '' + if len(response) > 0: + object_name, property_name, class_name = self._get_object_property_class_names(response[0].value) + if property_name is not None: + response_object = ', return_object=self.%s()' % property_name with open(api_filename, 'a') as self._fid: self._write() self._write(1, 'def %s(self%s):' % (method_name, content)) @@ -137,9 +143,7 @@ def _write_paths(self): self._write(0) self._write(2, '%s' % self._get_description(operation)) self._write(2, '"""') - self._write( - 2, "return self.send_recv('%s', '%s'%s)" % - (path['method'], path['url'], payload)) + self._write(2, "return self.send_recv('%s', '%s'%s%s)" % (path['method'], path['url'], payload, response_object)) # write top level objects for requests for yobject in self._openapi['paths'].values(): @@ -155,6 +159,12 @@ def _write_paths(self): object_name = top_level_schema['$ref'].split('/')[-1] property_name = object_name.lower().replace('.', '_') class_name = object_name.replace('.', '') + schema_object = self._get_object_from_ref(top_level_schema['$ref']) + if schema_object['type'] == 'array': + class_name = self._write_snappi_list(schema_object['items']['$ref'], property_name) + self._write_snappi_object(schema_object['items']['$ref']) + else: + self._write_snappi_object(top_level_schema['$ref']) with open(api_filename, 'a') as self._fid: self._write() self._write(1, 'def %s(self):' % property_name) @@ -166,7 +176,17 @@ def _write_paths(self): self._write( 2, "from .%s import %s" % (class_name.lower(), class_name)) self._write(2, "return %s()" % (class_name)) - self._write_snappi_object(top_level_schema['$ref']) + + def _get_object_property_class_names(self, schema): + object_name = None + property_name = None + class_name = None + if '$ref' in schema: + ref_name = schema['$ref'] + object_name = ref_name.split('/')[-1] + property_name = object_name.lower().replace('.', '_') + class_name = object_name.replace('.', '') + return (object_name, property_name, class_name) def _write_snappi_object(self, ref): schema_object = self._get_object_from_ref(ref) @@ -235,6 +255,7 @@ def _get_choice_names(self, schema_object): def _process_properties(self, class_name=None, schema_object=None): """Process all properties of a /component/schema object Write a factory method for all choice + If there are no properties then the schema_object is a primitive or array type """ refs = [] if 'properties' in schema_object: @@ -295,7 +316,7 @@ def _write_snappi_list(self, ref, property_name): # write factory method for the schema object in the list self._write_factory_method(None, class_name.lower(), ref, True) # write choice factory methods if any - if 'choice' in yobject['properties']: + if 'properties' in yobject and 'choice' in yobject['properties']: for property in yobject['properties']: if property not in yobject['properties']['choice'][ 'enum']: @@ -305,6 +326,7 @@ def _write_snappi_list(self, ref, property_name): ref = yobject['properties'][property]['$ref'] self._write_factory_method(class_name, property, ref, True) + return '%sList' % class_name def _write_factory_method(self, container_class_name, @@ -373,8 +395,7 @@ def _write_snappi_property(self, schema_object, name, property): self._write(1, 'def %s(self):' % name) self._write(2, '"""%s getter' % (name)) self._write() - for line in self._get_description(property): - self._write(2, line) + self._write(2, self._get_description(property)) self._write() self._write(2, 'Returns: %s' % self._get_type_restriction(property)) self._write(2, '"""') @@ -385,8 +406,7 @@ def _write_snappi_property(self, schema_object, name, property): self._write(1, 'def %s(self, value):' % name) self._write(2, '"""%s setter' % (name)) self._write() - for line in self._get_description(property): - self._write(2, line) + self._write(2, self._get_description(property)) self._write() self._write(2, 'value: %s' % self._get_type_restriction(property)) self._write(2, '"""') @@ -823,5 +843,5 @@ def _get_inline_ref(self, base_dir, filename, inline_key): if __name__ == '__main__': openapi_filename = None - openapi_filename = os.path.normpath('../../models/openapi.yaml') + # openapi_filename = os.path.normpath('../../models/openapi.yaml') SnappiGenerator(dependencies=False, openapi_filename=openapi_filename) diff --git a/snappi/tests/conftest.py b/snappi/tests/conftest.py index ab4b7e33..a16d182d 100644 --- a/snappi/tests/conftest.py +++ b/snappi/tests/conftest.py @@ -86,6 +86,19 @@ def get_config(): status=200) +@app.route('/results/port', methods=['POST']) +def get_port_metrics(): + import snappi + api = snappi.api.Api() + port_metrics_request = api.port_metrics_request() + port_metrics_request.deserialize(request.data.decode('utf-8')) + port_metrics = api.port_metrics() + port_metrics.portmetric().portmetric() + return Response(port_metrics.serialize(), + mimetype='application/json', + status=200) + + @app.after_request def after_request(resp): print(request.method, request.url, ' -> ', resp.status) diff --git a/snappi/tests/test_e2e_port_flow_config.py b/snappi/tests/test_e2e_port_flow_config.py new file mode 100644 index 00000000..6d0f214d --- /dev/null +++ b/snappi/tests/test_e2e_port_flow_config.py @@ -0,0 +1,47 @@ +import pytest + + +def test_e2e_port_flow_config(): + """Get port metrics + """ + import snappi + + api = snappi.api.Api() + + config = api.config() + + tx_port, rx_port = config.ports \ + .port(name='Tx Port', location='10.36.74.26;02;13') \ + .port(name='Rx Port', location='10.36.74.26;02;14') + + flow = config.flows.flow(name='Tx -> Rx Flow') + flow.tx_rx.port.tx_name = tx_port.name + flow.tx_rx.port.rx_name = rx_port.name + flow.size.fixed = 128 + flow.rate.pps = 1000 + flow.duration.fixed_packets.packets = 10000 + + eth, vlan, ip, tcp = flow.packet.ethernet().vlan().ipv4().tcp() + + eth.src.value = '00:00:01:00:00:01' + eth.dst.value_list = ['00:00:02:00:00:01', '00:00:02:00:00:01'] + eth.dst.result_group = 'eth dst mac' + + ip.src.increment.start = '1.1.1.1' + ip.src.increment.step = '0.0.0.1' + ip.src.increment.count = 10 + + ip.dst.decrement.start = '1.1.2.200' + ip.dst.decrement.step = '0.0.0.1' + ip.dst.decrement.count = 10 + + ip.priority.dscp.phb.value_list = [8, 16, 32] + ip.priority.dscp.ecn.value = 1 + tcp.src_port.increment.start = '10' + tcp.dst_port.increment.start = 1 + + print(config) + + +if __name__ == '__main__': + pytest.main(['-vv', '-s', __file__]) diff --git a/snappi/tests/test_port_metrics.py b/snappi/tests/test_port_metrics.py new file mode 100644 index 00000000..33e79338 --- /dev/null +++ b/snappi/tests/test_port_metrics.py @@ -0,0 +1,13 @@ +import pytest + + +def test_port_metrics(api): + """Get port metrics + """ + request = api.port_metrics_request() + port_metrics = api.get_port_metrics(request) + print(port_metrics) + + +if __name__ == '__main__': + pytest.main(['-vv', '-s', __file__]) diff --git a/snappi/tests/test_snappi_list_serialization.py b/snappi/tests/test_snappi_list_serialization.py new file mode 100644 index 00000000..bea0a4ad --- /dev/null +++ b/snappi/tests/test_snappi_list_serialization.py @@ -0,0 +1,15 @@ +import pytest + + +def test_snappi_list_serialization(api): + """Test serialization and deserialization of Snappi list objects + """ + port_metrics1 = api.port_metrics().portmetric().portmetric() + serialization = port_metrics1.serialize() + port_metrics2 = api.port_metrics() + port_metrics2.deserialize(serialization) + print(port_metrics2) + + +if __name__ == '__main__': + pytest.main(['-vv', '-s', __file__]) diff --git a/snappi/tests/test_serdes.py b/snappi/tests/test_snappi_object_serialization.py similarity index 69% rename from snappi/tests/test_serdes.py rename to snappi/tests/test_snappi_object_serialization.py index 77d78134..6422c5e8 100644 --- a/snappi/tests/test_serdes.py +++ b/snappi/tests/test_snappi_object_serialization.py @@ -2,8 +2,8 @@ @pytest.mark.parametrize('encoding', ['yaml', 'json', 'dict']) -def test_serdes(api, b2b_config, encoding): - """Demonstrate serialization and deserialization of Snappi objects +def test_snappi_object_serialization(api, b2b_config, encoding): + """Test serialization and deserialization of Snappi objects """ # serialize the configuration locally serialization1 = b2b_config.serialize(encoding) @@ -14,11 +14,6 @@ def test_serdes(api, b2b_config, encoding): # use a mock web server to pull the config config = api.get_config() - # TBD - # the following step should be removed when the code generation of - # responses is complete - config = api.config().deserialize(config) - # serialize the pulled config serialization2 = config.serialize(encoding)