diff --git a/backends/bmv2/base_test.py b/backends/bmv2/base_test.py index 15cda0ba5b..75c88ddc1c 100644 --- a/backends/bmv2/base_test.py +++ b/backends/bmv2/base_test.py @@ -6,12 +6,11 @@ import logging import queue import re -import socket import sys import threading import time from collections import Counter -from functools import partial, wraps +from functools import partialmethod, wraps from pathlib import Path import google.protobuf.text_format @@ -30,15 +29,6 @@ import testutils -# See https://gist.github.com/carymrobbins/8940382 -# functools.partialmethod is introduced in Python 3.4 -class partialmethod(partial): - def __get__(self, instance, owner): - if instance is None: - return self - return partial(self.func, instance, *(self.args or ()), **(self.keywords or {})) - - def stringify(n, length=0): """Take a non-negative integer 'n' as the first parameter, and a non-negative integer 'length' in units of _bytes_ as the second @@ -63,76 +53,6 @@ def stringify(n, length=0): return s -def ipv4_to_binary(addr): - """Take an argument 'addr' containing an IPv4 address written as a - string in dotted decimal notation, e.g. '10.1.2.3', and convert it - to a string with binary contents expected by the Python P4Runtime - client operations.""" - bytes_ = [int(b, 10) for b in addr.split(".")] - assert len(bytes_) == 4 - # Note: The bytes() call below will throw exception if any - # elements of bytes_ is outside of the range [0, 255]], so no need - # to add a separate check for that here. - return bytes(bytes_) - - -def ipv4_to_int(addr): - """Take an argument 'addr' containing an IPv4 address written as a - string in dotted decimal notation, e.g. '10.1.2.3', and convert it - to an integer.""" - bytes_ = [int(b, 10) for b in addr.split(".")] - assert len(bytes_) == 4 - # Note: The bytes() call below will throw exception if any - # elements of bytes_ is outside of the range [0, 255]], so no need - # to add a separate check for that here. - return int.from_bytes(bytes(bytes_), byteorder="big") - - -def ipv6_to_binary(addr): - """Take an argument 'addr' containing an IPv6 address written in - standard syntax, e.g. '2001:0db8::3210', and convert it to a - string with binary contents expected by the Python P4Runtime - client operations.""" - return socket.inet_pton(socket.AF_INET6, addr) - - -def ipv6_to_int(addr): - """Take an argument 'addr' containing an IPv6 address written in - standard syntax, e.g. '2001:0db8::3210', and convert it to an - integer.""" - bytes_ = socket.inet_pton(socket.AF_INET6, addr) - # Note: The bytes() call below will throw exception if any - # elements of bytes_ is outside of the range [0, 255]], so no need - # to add a separate check for that here. - return int.from_bytes(bytes_, byteorder="big") - - -def mac_to_binary(addr): - """Take an argument 'addr' containing an Ethernet MAC address written - as a string in hexadecimal notation, with each byte separated by a - colon, e.g. '00:de:ad:be:ef:ff', and convert it to a string with - binary contents expected by the Python P4Runtime client - operations.""" - bytes_ = [int(b, 16) for b in addr.split(":")] - assert len(bytes_) == 6 - # Note: The bytes() call below will throw exception if any - # elements of bytes_ is outside of the range [0, 255]], so no need - # to add a separate check for that here. - return bytes(bytes_) - - -def mac_to_int(addr): - """Take an argument 'addr' containing an Ethernet MAC address written - as a string in hexadecimal notation, with each byte separated by a - colon, e.g. '00:de:ad:be:ef:ff', and convert it to an integer.""" - bytes_ = [int(b, 16) for b in addr.split(":")] - assert len(bytes_) == 6 - # Note: The bytes() call below will throw exception if any - # elements of bytes_ is outside of the range [0, 255]], so no need - # to add a separate check for that here. - return int.from_bytes(bytes(bytes_), byteorder="big") - - # Used to indicate that the gRPC error Status object returned by the server has # an incorrect format. class P4RuntimeErrorFormatException(Exception): @@ -283,6 +203,7 @@ def setUp(self): self.dataplane = ptf.dataplane_instance self.dataplane.flush() + self.p4info_obj_map = {} self._swports = [] for _, port, _ in config["interfaces"]: self._swports.append(port) @@ -344,7 +265,6 @@ def updateConfig(self): # In order to make writing tests easier, we accept any suffix that uniquely # identifies the object among p4info objects of the same type. def import_p4info_names(self): - self.p4info_obj_map = {} suffix_count = Counter() for obj_type in [ "tables", @@ -353,6 +273,8 @@ def import_p4info_names(self): "counters", "direct_counters", "controller_packet_metadata", + "meters", + "direct_meters", ]: for obj in getattr(self.p4info, obj_type): pre = obj.preamble @@ -834,7 +756,7 @@ def send_request_add_entry_to_action(self, t_name, mk, a_name, params, priority= req = p4runtime_pb2.WriteRequest() req.device_id = self.device_id self.push_update_add_entry_to_action(req, t_name, mk, a_name, params, priority) - return req, self.write_request(req, store=(mk is not None)) + return req, self.write_request(req, store=mk is not None) def check_table_name_and_key(self, table_name_and_key): assert isinstance(table_name_and_key, tuple) @@ -893,7 +815,7 @@ def table_add(self, table_name_and_key, action_name_and_params, priority=None, o if key is None: update.type = p4runtime_pb2.Update.MODIFY testutils.log.info(f"table_add: req={req}") - return req, self.write_request(req, store=(key is not None)) + return req, self.write_request(req, store=key is not None) def pre_add_mcast_group(self, mcast_grp_id, port_instance_pair_list): """When a packet is sent from ingress to the packet buffer with @@ -985,6 +907,37 @@ def counter_dump_data(self, counter_name, direct=False): counter_entries.append(entry) return counter_entries + def meter_write(self, meter_name, index, meter_config): + req = self.get_new_write_request() + update = req.updates.add() + update.type = p4runtime_pb2.Update.MODIFY + entity = update.entity + meter_write = entity.meter_entry + meter_write.meter_id = self.get_meter_id(meter_name) + meter_write.index.index = index + meter_write.config.cir = meter_config.cir + meter_write.config.cburst = meter_config.cburst + meter_write.config.pir = meter_config.pir + meter_write.config.pburst = meter_config.pburst + return req, self.write_request(req) + + def direct_meter_write(self, meter_config, table_id, table_entry): + req = self.get_new_write_request() + update = req.updates.add() + update.type = p4runtime_pb2.Update.MODIFY + entity = update.entity + meter_write = entity.direct_meter_entry + meter_write.table_entry.table_id = table_id + if table_entry is None: + meter_write.table_entry.is_default_action = True + else: + meter_write.table_entry.CopyFrom(table_entry) + meter_write.config.cir = meter_config.cir + meter_write.config.cburst = meter_config.cburst + meter_write.config.pir = meter_config.pir + meter_write.config.pburst = meter_config.pburst + return req, self.write_request(req) + def make_table_read_request(self, table_name): req = p4runtime_pb2.ReadRequest() req.device_id = self.device_id @@ -993,6 +946,14 @@ def make_table_read_request(self, table_name): table.table_id = self.get_table_id(table_name) return req, table + def make_table_read_request_by_id(self, table_id): + req = p4runtime_pb2.ReadRequest() + req.device_id = self.device_id + entity = req.entities.add() + table = entity.table_entry + table.table_id = table_id + return req, table + def table_dump_data(self, table_name): req, table = self.make_table_read_request(table_name) table_entries = [] @@ -1043,7 +1004,7 @@ def send_request_add_entry_to_member(self, t_name, mk, mbr_id): req = p4runtime_pb2.WriteRequest() req.device_id = self.device_id self.push_update_add_entry_to_member(req, t_name, mk, mbr_id) - return req, self.write_request(req, store=(mk is not None)) + return req, self.write_request(req, store=mk is not None) def push_update_add_entry_to_group(self, req, t_name, mk, grp_id): update = req.updates.add() @@ -1060,7 +1021,7 @@ def send_request_add_entry_to_group(self, t_name, mk, grp_id): req = p4runtime_pb2.WriteRequest() req.device_id = self.device_id self.push_update_add_entry_to_group(req, t_name, mk, grp_id) - return req, self.write_request(req, store=(mk is not None)) + return req, self.write_request(req, store=mk is not None) # iterates over all requests in reverse order; if they are INSERT updates, # replay them as DELETE updates; this is a convenient way to clean-up a lot @@ -1103,6 +1064,8 @@ def get_new_write_request(self): ("actions", "action"), ("counters", "counter"), ("direct_counters", "direct_counter"), + ("meters", "meter"), + ("direct_meters", "direct_meter"), ("controller_packet_metadata", "controller_packet_metadata"), ]: name = "_".join(["get", nickname]) @@ -1151,7 +1114,7 @@ def update_config(config_path, p4info_path, grpc_addr, device_id): """ channel = grpc.insecure_channel(grpc_addr) stub = p4runtime_pb2_grpc.P4RuntimeStub(channel) - testutils.log.info(f"Sending P4 config from file {config_path} with P4info {p4info_path}") + testutils.log.info("Sending P4 config from file %s with P4info %s", config_path, p4info_path) request = p4runtime_pb2.SetForwardingPipelineConfigRequest() request.device_id = device_id config = request.config @@ -1162,7 +1125,7 @@ def update_config(config_path, p4info_path, grpc_addr, device_id): request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT try: response = stub.SetForwardingPipelineConfig(request) - logging.debug(f"Response {response}") + logging.debug("Response %s", response) except Exception as e: logging.error("Error during SetForwardingPipelineConfig") logging.error(e) diff --git a/backends/bmv2/run-bmv2-ptf-test.py b/backends/bmv2/run-bmv2-ptf-test.py index 036aa57192..863e8acc5b 100755 --- a/backends/bmv2/run-bmv2-ptf-test.py +++ b/backends/bmv2/run-bmv2-ptf-test.py @@ -200,7 +200,7 @@ def run_test(options: Options) -> int: return result -def create_options(test_args) -> Options: +def create_options(test_args) -> testutils.Optional[Options]: """Parse the input arguments and create a processed options object.""" options = Options() options.p4_file = Path(testutils.check_if_file(test_args.p4_file)) @@ -208,7 +208,10 @@ def create_options(test_args) -> Options: if not testfile: testutils.log.info("No test file provided. Checking for file in folder.") testfile = options.p4_file.with_suffix(".py") - options.testfile = Path(testutils.check_if_file(testfile)) + result = testutils.check_if_file(testfile) + if not result: + return None + options.testfile = Path(result) testdir = test_args.testdir if not testdir: testutils.log.info("No test directory provided. Generating temporary folder.") @@ -240,6 +243,8 @@ def create_options(test_args) -> Options: args, argv = PARSER.parse_known_args() test_options = create_options(args) + if not test_options: + sys.exit(testutils.FAILURE) # Run the test with the extracted options test_result = run_test(test_options) if not (args.nocleanup or test_result != testutils.SUCCESS): diff --git a/backends/p4tools/modules/testgen/lib/execution_state.cpp b/backends/p4tools/modules/testgen/lib/execution_state.cpp index 962adc18d6..035cb15272 100644 --- a/backends/p4tools/modules/testgen/lib/execution_state.cpp +++ b/backends/p4tools/modules/testgen/lib/execution_state.cpp @@ -169,7 +169,7 @@ const std::stack> } void ExecutionState::setProperty(cstring propertyName, Continuation::PropertyValue property) { - stateProperties[propertyName] = std::move(property); + stateProperties[propertyName] = property; } bool ExecutionState::hasProperty(cstring propertyName) const { diff --git a/backends/p4tools/modules/testgen/targets/bmv2/CMakeLists.txt b/backends/p4tools/modules/testgen/targets/bmv2/CMakeLists.txt index 98f5a2b2df..8bcc04d9ed 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/CMakeLists.txt +++ b/backends/p4tools/modules/testgen/targets/bmv2/CMakeLists.txt @@ -18,11 +18,12 @@ set( ${CMAKE_CURRENT_SOURCE_DIR}/constants.cpp ${CMAKE_CURRENT_SOURCE_DIR}/contrib/bmv2_hash/calculations.cpp ${CMAKE_CURRENT_SOURCE_DIR}/expr_stepper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/map_direct_externs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/p4_asserts_parser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/p4_refers_to_parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/program_info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/table_stepper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/target.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p4_asserts_parser.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/p4_refers_to_parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_backend.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_spec.cpp PARENT_SCOPE diff --git a/backends/p4tools/modules/testgen/targets/bmv2/backend/metadata/metadata.cpp b/backends/p4tools/modules/testgen/targets/bmv2/backend/metadata/metadata.cpp index 251c47eaf7..df260c4105 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/backend/metadata/metadata.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/backend/metadata/metadata.cpp @@ -66,7 +66,7 @@ inja::json Metadata::getVerify(const TestSpec *testSpec) { inja::json verifyData = inja::json::object(); auto egressPacket = testSpec->getEgressPacket(); if (egressPacket.has_value()) { - const auto *const packet = egressPacket.value(); + const auto *packet = egressPacket.value(); verifyData["eg_port"] = packet->getPort(); const auto *payload = packet->getEvaluatedPayload(); const auto *payloadMask = packet->getEvaluatedPayloadMask(); @@ -94,7 +94,7 @@ statement_coverage: {{coverage}} ## endfor # The input packet in hexadecimal. -input_packet: \"{{send.pkt}}\" +input_packet: "{{send.pkt}}" # Parsed headers and their offsets. header_offsets: @@ -105,7 +105,7 @@ input_packet: \"{{send.pkt}}\" # Metadata results after this test has completed. metadata: ## for metadata_field in metadata_fields - {{metadata_field.name}}: [value: \"{{metadata_field.value}}\", offset: {{metadata_field.offset}}] + {{metadata_field.name}}: [value: "{{metadata_field.value}}", offset: {{metadata_field.offset}}] ## endfor )"""); return TEST_CASE; diff --git a/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.cpp b/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.cpp index 3c6ac5b6c8..d0ea6d2235 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.cpp @@ -45,6 +45,28 @@ inja::json PTF::getClone(const TestObjectMap &cloneSpecs) { return cloneSpec; } +inja::json::array_t PTF::getMeter(const TestObjectMap &meterValues) { + auto meterJson = inja::json::array_t(); + for (auto meterValueInfoTuple : meterValues) { + const auto *meterValue = meterValueInfoTuple.second->checkedTo(); + + const auto meterEntries = meterValue->unravelMap(); + for (const auto &meterEntry : meterEntries) { + inja::json meterInfoJson; + meterInfoJson["name"] = meterValueInfoTuple.first; + meterInfoJson["value"] = formatHexExpr(meterEntry.second.second); + meterInfoJson["index"] = formatHex(meterEntry.first, meterEntry.second.first); + if (meterValue->isDirectMeter()) { + meterInfoJson["is_direct"] = "True"; + } else { + meterInfoJson["is_direct"] = "False"; + } + meterJson.push_back(meterInfoJson); + } + } + return meterJson; +} + std::vector> PTF::getIgnoreMasks(const IR::Constant *mask) { std::vector> ignoreMasks; if (mask == nullptr) { @@ -189,11 +211,12 @@ inja::json PTF::getSend(const TestSpec *testSpec) { inja::json PTF::getVerify(const TestSpec *testSpec) { inja::json verifyData = inja::json::object(); - if (testSpec->getEgressPacket() != std::nullopt) { - const auto &packet = **testSpec->getEgressPacket(); - verifyData["eg_port"] = packet.getPort(); - const auto *payload = packet.getEvaluatedPayload(); - const auto *payloadMask = packet.getEvaluatedPayloadMask(); + auto egressPacket = testSpec->getEgressPacket(); + if (egressPacket.has_value()) { + const auto *packet = egressPacket.value(); + verifyData["eg_port"] = packet->getPort(); + const auto *payload = packet->getEvaluatedPayload(); + const auto *payloadMask = packet->getEvaluatedPayloadMask(); verifyData["ignore_masks"] = getIgnoreMasks(payloadMask); auto dataStr = formatHexExpr(payload, false, true, false); @@ -202,14 +225,12 @@ inja::json PTF::getVerify(const TestSpec *testSpec) { return verifyData; } -static std::string getPreamble() { +void PTF::emitPreamble() { static const std::string PREAMBLE( R"""(# P4Runtime PTF test for {{test_name}} # p4testgen seed: {{ default(seed, "none") }} -import logging -import sys -import os +from enum import Enum from ptf.mask import Mask @@ -219,7 +240,9 @@ from ptf import testutils as ptfutils import base_test as bt + class AbstractTest(bt.P4RuntimeTest): + EnumColor = Enum("EnumColor", ["GREEN", "YELLOW", "RED"], start=0) @bt.autocleanup def setUp(self): @@ -246,19 +269,48 @@ class AbstractTest(bt.P4RuntimeTest): bt.testutils.log.info("Verifying Packet ...") self.verifyPackets() - + def meter_write_with_predefined_config(self, meter_name, index, value, direct): + """Since we can not blast the target with packets, we have to carefully craft an artificial scenario where the meter will return the color we want. We do this by setting the meter config in such a way that the meter is forced to assign the desired color. For example, for RED to the lowest threshold values, to force a RED assignment.""" + value = self.EnumColor(value) + if value == self.EnumColor.GREEN: + meter_config = bt.p4runtime_pb2.MeterConfig( + cir=4294967295, cburst=4294967295, pir=4294967295, pburst=4294967295 + ) + elif value == self.EnumColor.YELLOW: + meter_config = bt.p4runtime_pb2.MeterConfig( + cir=1, cburst=1, pir=4294967295, pburst=4294967295 + ) + elif value == self.EnumColor.RED: + meter_config = bt.p4runtime_pb2.MeterConfig( + cir=1, cburst=1, pir=1, pburst=1 + ) + else: + raise self.failureException(f"Unsupported meter value {value}") + if direct: + meter_obj = self.get_obj("direct_meters", meter_name) + table_id = meter_obj.direct_table_id + req, _ = self.make_table_read_request_by_id(table_id) + table_entry = None + for response in self.response_dump_helper(req): + for entity in response.entities: + assert entity.WhichOneof("entity") == "table_entry" + table_entry = entity.table_entry + break + if table_entry is None: + raise self.failureException( + "No entry in the table that the meter is attached to." + ) + return self.direct_meter_write(meter_config, table_id, table_entry) + return self.meter_write(meter_name, index, meter_config) )"""); - return PREAMBLE; -} -void PTF::emitPreamble(const std::string &preamble) { inja::json dataJson; dataJson["test_name"] = basePath.stem(); if (seed) { dataJson["seed"] = *seed; } - inja::render_to(ptfFileStream, preamble, dataJson); + inja::render_to(ptfFileStream, PREAMBLE, dataJson); ptfFileStream.flush(); } @@ -319,6 +371,10 @@ class Test{{test_id}}(AbstractTest): self.insert_pre_clone_session({{clone_pkt.session_id}}, [{{clone_pkt.clone_port}}]) ## endfor ## endif +## for meter_value in meter_values + self.meter_write_with_predefined_config("{{meter_value.name}}", {{meter_value.index}}, {{meter_value.value}}, {{meter_value.is_direct}}) +## endfor + def sendPacket(self): ## if send @@ -385,6 +441,8 @@ void PTF::emitTestcase(const TestSpec *testSpec, cstring selectedBranches, size_ if (!cloneSpecs.empty()) { dataJson["clone_specs"] = getClone(cloneSpecs); } + auto meterValues = testSpec->getTestObjectCategory("meter_values"); + dataJson["meter_values"] = getMeter(meterValues); LOG5("PTF backend: emitting testcase:" << std::setw(4) << dataJson); @@ -398,8 +456,7 @@ void PTF::outputTest(const TestSpec *testSpec, cstring selectedBranches, size_t auto ptfFile = basePath; ptfFile.replace_extension(".py"); ptfFileStream = std::ofstream(ptfFile); - std::string preamble = getPreamble(); - emitPreamble(preamble); + emitPreamble(); preambleEmitted = true; } std::string testCase = getTestCaseTemplate(); diff --git a/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.h b/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.h index c8d9367fbd..72815e1b74 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.h +++ b/backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.h @@ -49,7 +49,7 @@ class PTF : public TF { private: /// Emits the test preamble. This is only done once for all generated tests. /// For the PTF back end this is the test setup Python script.. - void emitPreamble(const std::string &preamble); + void emitPreamble(); /// Emits a test case. /// @param testIdx specifies the test name. @@ -71,7 +71,10 @@ class PTF : public TF { /// Converts the output packet, port, and mask into Inja format. static inja::json getVerify(const TestSpec *testSpec); - /// Returns the configuration for a cloned packet configuration. + /// @returns the configuration for a meter call (may set the meter to GREEN, YELLOW, or RED) + static inja::json::array_t getMeter(const TestObjectMap &meterValues); + + /// @returns the configuration for a cloned packet configuration. static inja::json getClone(const TestObjectMap &cloneSpecs); /// Helper function for @getVerify. Matches the mask value against the input packet value and diff --git a/backends/p4tools/modules/testgen/targets/bmv2/constants.h b/backends/p4tools/modules/testgen/targets/bmv2/constants.h index 914af672e8..51d251e813 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/constants.h +++ b/backends/p4tools/modules/testgen/targets/bmv2/constants.h @@ -36,6 +36,8 @@ class BMv2Constants { static constexpr int STF_MIN_PKT_SIZE = 22; static constexpr int ETH_HDR_SIZE = 112; static constexpr int DROP_PORT = 511; + + enum METER_COLOR { GREEN = 0, YELLOW = 1, RED = 2 }; }; } // namespace P4Tools::P4Testgen::Bmv2 diff --git a/backends/p4tools/modules/testgen/targets/bmv2/expr_stepper.cpp b/backends/p4tools/modules/testgen/targets/bmv2/expr_stepper.cpp index 473cdf0376..5c0e63dd5a 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/expr_stepper.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/expr_stepper.cpp @@ -18,6 +18,7 @@ #include "backends/p4tools/common/lib/variables.h" #include "ir/declaration.h" #include "ir/indexed_vector.h" +#include "ir/ir-generated.h" #include "ir/irutils.h" #include "ir/node.h" #include "lib/cstring.h" @@ -654,25 +655,25 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression std::vector replacements; const auto *receiverPath = receiver->checkedTo(); - const auto &externInstance = state.convertPathExpr(receiverPath); + const auto &externInstance = nextState.findDecl(receiverPath); // Retrieve the register state from the object store. If it is already present, // just cast the object to the correct class and retrieve the current value // according to the index. If the register has not been added had, create a new // register object. const auto *registerState = - state.getTestObject("registervalues", externInstance->toString(), false); - const Bmv2RegisterValue *registerValue = nullptr; + state.getTestObject("registervalues", externInstance->controlPlaneName(), false); + const Bmv2V1ModelRegisterValue *registerValue = nullptr; if (registerState != nullptr) { - registerValue = registerState->checkedTo(); + registerValue = registerState->checkedTo(); } else { const auto *inputValue = programInfo.createTargetUninitialized(readOutput->type, false); - registerValue = new Bmv2RegisterValue(inputValue); - nextState.addTestObject("registervalues", externInstance->toString(), + registerValue = new Bmv2V1ModelRegisterValue(inputValue); + nextState.addTestObject("registervalues", externInstance->controlPlaneName(), registerValue); } - const IR::Expression *baseExpr = registerValue->getCurrentValue(index); + const IR::Expression *baseExpr = registerValue->getValueAtIndex(index); if (readOutput->type->is()) { // We need an assignment statement (and the inefficient copy) here because we @@ -753,9 +754,9 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression return; } } - const auto *receiverPath = receiver->checkedTo(); - const auto &externInstance = state.convertPathExpr(receiverPath); auto &nextState = state.clone(); + const auto *receiverPath = receiver->checkedTo(); + const auto &externInstance = nextState.findDecl(receiverPath); // TODO: Find a better way to model a trace of this event. std::stringstream registerStream; registerStream << "RegisterWrite: Value "; @@ -764,23 +765,23 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression index->dbprint(registerStream); nextState.add(*new TraceEvents::Generic(registerStream.str())); - // "Write" to the register by update the internal test object state. If the - // register did not exist previously, update it with the value to write as initial - // value. - const auto *registerState = - nextState.getTestObject("registervalues", externInstance->toString(), false); - Bmv2RegisterValue *registerValue = nullptr; + // "Write" to the register by update the internal test object state. If the register + // did not exist previously, update it with the value to write as initial value. + const auto *registerState = nextState.getTestObject( + "registervalues", externInstance->controlPlaneName(), false); + Bmv2V1ModelRegisterValue *registerValue = nullptr; if (registerState != nullptr) { - registerValue = - new Bmv2RegisterValue(*registerState->checkedTo()); - registerValue->addRegisterCondition(Bmv2RegisterCondition{index, inputValue}); + registerValue = new Bmv2V1ModelRegisterValue( + *registerState->checkedTo()); + registerValue->writeToIndex(index, inputValue); } else { const auto &writeValue = programInfo.createTargetUninitialized(inputValue->type, false); - registerValue = new Bmv2RegisterValue(writeValue); - registerValue->addRegisterCondition(Bmv2RegisterCondition{index, inputValue}); + registerValue = new Bmv2V1ModelRegisterValue(writeValue); + registerValue->writeToIndex(index, inputValue); } - nextState.addTestObject("registervalues", externInstance->toString(), registerValue); + nextState.addTestObject("registervalues", externInstance->controlPlaneName(), + registerValue); nextState.popBody(); result->emplace_back(nextState); }}, @@ -817,7 +818,6 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression [](const IR::MethodCallExpression * /*call*/, const IR::Expression * /*receiver*/, IR::ID & /*methodName*/, const IR::Vector * /*args*/, const ExecutionState &state, SmallStepEvaluator::Result &result) { - ::warning("counter.count not fully implemented."); auto &nextState = state.clone(); nextState.popBody(); result->emplace_back(nextState); @@ -855,13 +855,12 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression [](const IR::MethodCallExpression * /*call*/, const IR::Expression * /*receiver*/, IR::ID & /*methodName*/, const IR::Vector * /*args*/, const ExecutionState &state, SmallStepEvaluator::Result &result) { - ::warning("direct_counter.count not fully implemented."); auto &nextState = state.clone(); nextState.popBody(); result->emplace_back(nextState); }}, /* ====================================================================================== - * meter.read + * meter.execute_meter * A meter object is created by calling its constructor. This * creates an array of meter states, with the number of meter * states specified by the size parameter. The array indices are @@ -890,21 +889,98 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression * meaning of these colors). When index is out of * range, the final value of result is not specified, * and should be ignored by the caller. - * ====================================================================================== - */ - // TODO: Read currently has no effect in the symbolic interpreter. + * ====================================================================================== */ {"meter.execute_meter", {"index", "result"}, - [](const IR::MethodCallExpression * /*call*/, const IR::Expression * /*receiver*/, - IR::ID & /*methodName*/, const IR::Vector * /*args*/, + [](const IR::MethodCallExpression *call, const IR::Expression *receiver, + IR::ID & /*methodName*/, const IR::Vector *args, const ExecutionState &state, SmallStepEvaluator::Result &result) { - ::warning("meter.execute_meter not fully implemented."); + auto testBackend = TestgenOptions::get().testBackend; + if (testBackend != "PTF") { + ::warning( + "meter.execute_meter not implemented for %1%. Choosing default value (GREEN).", + testBackend); + auto &nextState = state.clone(); + const IR::Member *meterResult = nullptr; + const auto *expr = args->at(1)->expression; + if (const auto *pathRef = expr->to()) { + meterResult = ExecutionState::convertPathExpr(pathRef); + } else if (const auto *member = expr->to()) { + meterResult = member; + } else { + TESTGEN_UNIMPLEMENTED("Unsupported meter result variable %1% of type %2%", + expr, expr->node_type_name()); + } + nextState.set(meterResult, IR::getConstant(meterResult->type, + BMv2Constants::METER_COLOR::GREEN)); + nextState.popBody(); + result->emplace_back(nextState); + return; + } + const auto *meterResult = args->at(1)->expression; + + // TODO: Frontload this in the expression stepper for method call expressions. + const auto *index = args->at(0)->expression; + if (!SymbolicEnv::isSymbolicValue(index)) { + // Evaluate the condition. + stepToSubexpr(index, result, state, [call](const Continuation::Parameter *v) { + auto *clonedCall = call->clone(); + auto *arguments = clonedCall->arguments->clone(); + auto *arg = arguments->at(0)->clone(); + arg->expression = v->param; + (*arguments)[0] = arg; + clonedCall->arguments = arguments; + return Continuation::Return(clonedCall); + }); + return; + } auto &nextState = state.clone(); - nextState.popBody(); - result->emplace_back(nextState); + std::vector replacements; + + const auto *receiverPath = receiver->checkedTo(); + const auto &externInstance = nextState.findDecl(receiverPath); + + // Retrieve the meter state from the object store. If it is already present, just + // cast the object to the correct class and retrieve the current value according to the + // index. If the meter has not been added had, create a new meter object. + const auto *meterState = + state.getTestObject("meter_values", externInstance->controlPlaneName(), false); + Bmv2V1ModelMeterValue *meterValue = nullptr; + const auto &inputValue = nextState.createSymbolicVariable( + meterResult->type, "meter_value" + std::to_string(call->clone_id)); + // Make sure we do not accidentally get "3" as enum assignment... + auto *cond = new IR::Lss(inputValue, IR::getConstant(meterResult->type, 3)); + if (meterState != nullptr) { + meterValue = + new Bmv2V1ModelMeterValue(*meterState->checkedTo()); + } else { + meterValue = new Bmv2V1ModelMeterValue(inputValue, false); + } + meterValue->writeToIndex(index, inputValue); + nextState.addTestObject("meter_values", externInstance->controlPlaneName(), + meterValue); + + if (meterResult->type->is()) { + // We need an assignment statement (and the inefficient copy) here because we need + // to immediately resolve the generated mux into multiple branches. + // This is only possible because meters do not return a value. + replacements.emplace_back(new IR::AssignmentStatement(meterResult, inputValue)); + } else { + TESTGEN_UNIMPLEMENTED("Read extern output %1% of type %2% not supported", + meterResult, meterResult->type); + } + // TODO: Find a better way to model a trace of this event. + std::stringstream meterStream; + meterStream << "MeterExecute: Index "; + index->dbprint(meterStream); + meterStream << " into field "; + meterResult->dbprint(meterStream); + nextState.add(*new TraceEvents::Generic(meterStream.str())); + nextState.replaceTopBody(&replacements); + result->emplace_back(cond, state, nextState); }}, /* ====================================================================================== - * direct_meter.count + * direct_meter.read * A direct_meter object is created by calling its constructor. * You must provide a choice of whether to meter based on the * number of packets, regardless of their size @@ -932,18 +1008,84 @@ void Bmv2V1ModelExprStepper::evalExternMethodCall(const IR::MethodCallExpression * result will be assigned 0 for color GREEN, 1 for * color YELLOW, and 2 for color RED (see RFC 2697 * and RFC 2698 for the meaning of these colors). - * ====================================================================================== - */ - // TODO: Read currently has no effect in the symbolic interpreter. + * ====================================================================================== */ {"direct_meter.read", {"result"}, - [](const IR::MethodCallExpression * /*call*/, const IR::Expression * /*receiver*/, - IR::ID & /*methodName*/, const IR::Vector * /*args*/, - const ExecutionState &state, SmallStepEvaluator::Result &result) { - ::warning("direct_meter.read not fully implemented."); + [this](const IR::MethodCallExpression *call, const IR::Expression *receiver, + IR::ID & /*methodName*/, const IR::Vector *args, + const ExecutionState &state, SmallStepEvaluator::Result &result) { + // Check whether table that the extern is attached to has an entry. + const auto *progInfo = getProgramInfo().checkedTo(); + const auto *receiverPath = receiver->checkedTo(); + const auto *externInstance = state.findDecl(receiverPath); + const auto *table = progInfo->getTableofDirectExtern(externInstance); + const auto *tableEntry = + state.getTestObject("tableconfigs", table->controlPlaneName(), false); + auto &nextState = state.clone(); - nextState.popBody(); - result->emplace_back(nextState); + std::vector replacements; + const auto *expr = args->at(0)->expression; + const IR::Member *meterResult = nullptr; + if (const auto *pathRef = expr->to()) { + meterResult = ExecutionState::convertPathExpr(pathRef); + } else if (const auto *member = expr->to()) { + meterResult = member; + } else { + TESTGEN_UNIMPLEMENTED("Unsupported meter result variable %1% of type %2%", expr, + expr->node_type_name()); + } + // If we do not have right back end and no associated table entry, do not bother + // configuring the extern. We just set the default value (green). + auto testBackend = TestgenOptions::get().testBackend; + if (testBackend != "PTF" || tableEntry == nullptr) { + ::warning( + "direct_meter.read configuration not possible for %1%. Choosing default value " + "(GREEN).", + testBackend); + nextState.set(meterResult, IR::getConstant(meterResult->type, + BMv2Constants::METER_COLOR::GREEN)); + nextState.popBody(); + result->emplace_back(nextState); + return; + } + + // Retrieve the meter state from the object store. If it is already present, just + // cast the object to the correct class and retrieve the current value according to the + // index. If the meter has not been added had, create a new meter object. + const auto *meterState = + state.getTestObject("meter_values", externInstance->controlPlaneName(), false); + Bmv2V1ModelMeterValue *meterValue = nullptr; + const auto &inputValue = nextState.createSymbolicVariable( + meterResult->type, "meter_value" + std::to_string(call->clone_id)); + // Make sure we do not accidentally get "3" as enum assignment... + auto *cond = new IR::Lss(inputValue, IR::getConstant(meterResult->type, 3)); + if (meterState != nullptr) { + meterValue = + new Bmv2V1ModelMeterValue(*meterState->checkedTo()); + } else { + meterValue = new Bmv2V1ModelMeterValue(inputValue, true); + } + meterValue->writeToIndex(IR::getConstant(IR::getBitType(1), 0), inputValue); + nextState.addTestObject("meter_values", externInstance->controlPlaneName(), + meterValue); + + if (meterResult->type->is()) { + // We need an assignment statement (and the inefficient copy) here because we need + // to immediately resolve the generated mux into multiple branches. + // This is only possible because meters do not return a value. + replacements.emplace_back(new IR::AssignmentStatement(meterResult, inputValue)); + } else { + TESTGEN_UNIMPLEMENTED("Read extern output %1% of type %2% not supported", + meterResult, meterResult->type); + } + // TODO: Find a better way to model a trace of this event. + std::stringstream meterStream; + meterStream << "MeterRead: into field "; + meterResult->dbprint(meterStream); + nextState.add(*new TraceEvents::Generic(meterStream.str())); + nextState.replaceTopBody(&replacements); + result->emplace_back(cond, state, nextState); + return; }}, /* ====================================================================================== diff --git a/backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.cpp b/backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.cpp new file mode 100644 index 0000000000..c29a79caaa --- /dev/null +++ b/backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.cpp @@ -0,0 +1,49 @@ +#include "backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.h" + +namespace P4Tools::P4Testgen::Bmv2 { + +bool MapDirectExterns::preorder(const IR::Declaration_Instance *declInstance) { + declaredExterns[declInstance->name] = declInstance; + return true; +} + +bool MapDirectExterns::preorder(const IR::P4Table *table) { + // Try to either get counters or meters from the table. + const auto *impl = table->properties->getProperty("meters"); + if (impl == nullptr) { + impl = table->properties->getProperty("counters"); + } + if (impl == nullptr) { + return false; + } + // Cannot map a temporary mapped direct extern without a counter. + const auto *selectorExpr = impl->value->checkedTo(); + if (selectorExpr->expression->is()) { + return true; + } + // Try to find the extern in the declared instances. + const auto *implPath = selectorExpr->expression->to(); + auto implementationLabel = implPath->path->name.name; + + // If the extern is not in the list of declared instances, move on. + auto it = declaredExterns.find(implementationLabel); + if (it == declaredExterns.end()) { + return true; + } + // BMv2 does not support direct externs attached to multiple tables. + const auto *declInstance = it->second; + if (directExternMap.find(declInstance) != directExternMap.end()) { + FATAL_ERROR( + "Direct extern was already mapped to a table. It can not be attached to two tables. "); + return true; + } + directExternMap.emplace(declInstance, table); + return true; +} + +const std::map + &MapDirectExterns::getdirectExternMap() { + return directExternMap; +} + +} // namespace P4Tools::P4Testgen::Bmv2 diff --git a/backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.h b/backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.h new file mode 100644 index 0000000000..7231794571 --- /dev/null +++ b/backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.h @@ -0,0 +1,33 @@ +#ifndef BACKENDS_P4TOOLS_MODULES_TESTGEN_TARGETS_BMV2_MAP_DIRECT_EXTERNS_H_ +#define BACKENDS_P4TOOLS_MODULES_TESTGEN_TARGETS_BMV2_MAP_DIRECT_EXTERNS_H_ + +#include + +#include "ir/ir.h" +#include "ir/visitor.h" + +namespace P4Tools::P4Testgen::Bmv2 { + +/// A lightweight visitor, which collects all the declarations in the program then checks whether a +/// table is referencing the declaration as an direct extern. The direct externs are collected in a +/// map. Currently checks for "counters" or "meters" properties in the table. +class MapDirectExterns : public Inspector { + private: + /// List of the declared instances in the program. + std::map declaredExterns; + + /// Maps direct extern declarations to the table they are attached to. + std::map directExternMap; + + public: + bool preorder(const IR::Declaration_Instance *declInstance) override; + + bool preorder(const IR::P4Table *table) override; + + /// @returns the list of direct externs and their corresponding table. + const std::map &getdirectExternMap(); +}; + +} // namespace P4Tools::P4Testgen::Bmv2 + +#endif /*BACKENDS_P4TOOLS_MODULES_TESTGEN_TARGETS_BMV2_MAP_DIRECT_EXTERNS_H_*/ diff --git a/backends/p4tools/modules/testgen/targets/bmv2/program_info.cpp b/backends/p4tools/modules/testgen/targets/bmv2/program_info.cpp index a7cbc27b28..8278f85316 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/program_info.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/program_info.cpp @@ -30,6 +30,7 @@ #include "backends/p4tools/modules/testgen/options.h" #include "backends/p4tools/modules/testgen/targets/bmv2/concolic.h" #include "backends/p4tools/modules/testgen/targets/bmv2/constants.h" +#include "backends/p4tools/modules/testgen/targets/bmv2/map_direct_externs.h" #include "backends/p4tools/modules/testgen/targets/bmv2/p4_asserts_parser.h" #include "backends/p4tools/modules/testgen/targets/bmv2/p4_refers_to_parser.h" @@ -76,23 +77,40 @@ Bmv2V1ModelProgramInfo::Bmv2V1ModelProgramInfo( const IR::Expression *constraint = new IR::Grt(IR::Type::Boolean::get(), ExecutionState::getInputPacketSizeVar(), IR::getConstant(&PacketVars::PACKET_SIZE_VAR_TYPE, minPktSize)); - /// Vector containing pairs of restrictions and nodes to which these restrictions apply. + // Vector containing pairs of restrictions and nodes to which these restrictions apply. std::vector> restrictionsVec; - /// Defines all "entry_restriction" and then converts restrictions from string to IR - /// expressions, and stores them in restrictionsVec to move targetConstraints further. + // Defines all "entry_restriction" and then converts restrictions from string to IR + // expressions, and stores them in restrictionsVec to move targetConstraints further. program->apply(AssertsParser::AssertsParser(restrictionsVec)); - /// Defines all "refers_to" and then converts restrictions from string to IR expressions, - /// and stores them in restrictionsVec to move targetConstraints further. + // Defines all "refers_to" and then converts restrictions from string to IR expressions, + // and stores them in restrictionsVec to move targetConstraints further. program->apply(RefersToParser::RefersToParser(restrictionsVec)); for (const auto &element : restrictionsVec) { for (const auto *restriction : element) { constraint = new IR::LAnd(constraint, restriction); } } + // Try to map all instances of direct externs to the table they are attached to. + // Save the map in @var directExternMap. + auto directExternMapper = MapDirectExterns(); + program->apply(directExternMapper); + auto mappedDirectExterns = directExternMapper.getdirectExternMap(); + directExternMap.insert(mappedDirectExterns.begin(), mappedDirectExterns.end()); + /// Finally, set the target constraints. targetConstraints = constraint; } +const IR::P4Table *Bmv2V1ModelProgramInfo::getTableofDirectExtern( + const IR::IDeclaration *directExternDecl) const { + auto it = directExternMap.find(directExternDecl); + if (it == directExternMap.end()) { + BUG("No table associated with this direct extern %1%. The extern should have been removed.", + directExternDecl); + } + return it->second; +} + const ordered_map *Bmv2V1ModelProgramInfo::getProgrammableBlocks() const { return &programmableBlocks; diff --git a/backends/p4tools/modules/testgen/targets/bmv2/program_info.h b/backends/p4tools/modules/testgen/targets/bmv2/program_info.h index a964c2030f..e41b43b389 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/program_info.h +++ b/backends/p4tools/modules/testgen/targets/bmv2/program_info.h @@ -31,6 +31,8 @@ class Bmv2V1ModelProgramInfo : public ProgramInfo { std::vector processDeclaration(const IR::Type_Declaration *typeDecl, size_t blockIdx) const; + std::map directExternMap; + public: Bmv2V1ModelProgramInfo(const IR::P4Program *program, ordered_map inputBlocks, @@ -39,6 +41,9 @@ class Bmv2V1ModelProgramInfo : public ProgramInfo { /// @returns the gress associated with the given parser. int getGress(const IR::Type_Declaration *) const; + /// @returns the table associated with the direct extern + const IR::P4Table *getTableofDirectExtern(const IR::IDeclaration *directExternDecl) const; + /// @returns the programmable blocks of the program. Should be 6. [[nodiscard]] const ordered_map *getProgrammableBlocks() const; diff --git a/backends/p4tools/modules/testgen/targets/bmv2/table_stepper.cpp b/backends/p4tools/modules/testgen/targets/bmv2/table_stepper.cpp index dd7d1b87ef..b3180f504d 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/table_stepper.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/table_stepper.cpp @@ -304,7 +304,7 @@ bool Bmv2V1ModelTableStepper::checkForActionProfile() { } const auto *testObject = - state->getTestObject("action_profile", implExtern->controlPlaneName(), false); + state->getTestObject("action_profile", implDecl->controlPlaneName(), false); if (testObject == nullptr) { // This means, for every possible control plane entry (and with that, new execution state) // add the generated action profile. @@ -347,7 +347,7 @@ bool Bmv2V1ModelTableStepper::checkForActionSelector() { // Treat action selectors like action profiles for now. // The behavioral model P4Runtime is unclear how to configure action selectors. const auto *testObject = - state->getTestObject("action_profile", selectorExtern->controlPlaneName(), false); + state->getTestObject("action_profile", selectorDecl->controlPlaneName(), false); if (testObject == nullptr) { // This means, for every possible control plane entry (and with that, new execution state) // add the generated action profile. diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_direct_meter_1.p4 b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_direct_meter_1.p4 new file mode 100644 index 0000000000..6d612f23df --- /dev/null +++ b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_direct_meter_1.p4 @@ -0,0 +1,81 @@ +#include +#include + +header ethernet_t { + bit<48> dst_addr; + bit<48> src_addr; + bit<16> eth_type; +} + +header H { + bit<8> color_status; + bit<32> idx; +} + +struct headers { + ethernet_t eth_hdr; + H h; +} + +enum bit<2> MeterColor { + GREEN = 0, + YELLOW = 1, + RED = 2 +} + +struct Meta {} + +parser p(packet_in pkt, out headers hdr, inout Meta m, inout standard_metadata_t sm) { + state start { + transition parse_hdrs; + } + state parse_hdrs { + pkt.extract(hdr.eth_hdr); + pkt.extract(hdr.h); + transition accept; + } +} + +control ingress(inout headers h, inout Meta m, inout standard_metadata_t sm) { + direct_meter(MeterType.bytes) table_attached_meter; + MeterColor color = (MeterColor) 0; + + action meter_assign() { + table_attached_meter.read(color); + } + table meter_table { + actions = { + meter_assign; + } + key = { + h.eth_hdr.dst_addr: ternary; + } + size = 16384; + default_action = meter_assign(); + meters = table_attached_meter; + } + + apply { + meter_table.apply(); + if (color == MeterColor.RED) { + h.h.color_status = 2; + } else if (color == MeterColor.YELLOW) { + h.h.color_status = 1; + } else { + h.h.color_status = 0; + } + } +} + +control vrfy(inout headers h, inout Meta m) { apply {} } + +control update(inout headers h, inout Meta m) { apply {} } + +control egress(inout headers h, inout Meta m, inout standard_metadata_t sm) { apply {} } + +control deparser(packet_out pkt, in headers h) { + apply { + pkt.emit(h); + } +} +V1Switch(p(), vrfy(), ingress(), egress(), update(), deparser()) main; diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_meter_1.p4 b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_meter_1.p4 new file mode 100644 index 0000000000..2502ab9c9a --- /dev/null +++ b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_meter_1.p4 @@ -0,0 +1,65 @@ +#include +#include + +header ethernet_t { + bit<48> dst_addr; + bit<48> src_addr; + bit<16> eth_type; +} + +header H { + bit<8> color_status; + bit<32> idx; +} + +struct headers { + ethernet_t eth_hdr; + H h; +} + +enum bit<2> MeterColor { + GREEN = 0, + YELLOW = 1, + RED = 2 +} + +struct Meta {} + +parser p(packet_in pkt, out headers hdr, inout Meta m, inout standard_metadata_t sm) { + state start { + transition parse_hdrs; + } + state parse_hdrs { + pkt.extract(hdr.eth_hdr); + pkt.extract(hdr.h); + transition accept; + } +} + +control ingress(inout headers h, inout Meta m, inout standard_metadata_t sm) { + meter(1024, MeterType.bytes) simple_meter; + apply { + MeterColor color; + simple_meter.execute_meter(h.h.idx, color); + if (color == MeterColor.RED) { + h.h.color_status = 2; + } else if (color == MeterColor.YELLOW) { + h.h.color_status = 1; + } else { + h.h.color_status = 0; + } + } +} + +control vrfy(inout headers h, inout Meta m) { apply {} } + +control update(inout headers h, inout Meta m) { apply {} } + +control egress(inout headers h, inout Meta m, inout standard_metadata_t sm) { apply {} } + +control deparser(packet_out pkt, in headers h) { + apply { + pkt.emit(h); + } +} +V1Switch(p(), vrfy(), ingress(), egress(), update(), deparser()) main; diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_meter_2.p4 b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_meter_2.p4 new file mode 100644 index 0000000000..8394decbd6 --- /dev/null +++ b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_meter_2.p4 @@ -0,0 +1,74 @@ +#include +#include + +header ethernet_t { + bit<48> dst_addr; + bit<48> src_addr; + bit<16> eth_type; +} + +header H { + bit<8> color_status; + bit<8> color_status_2; + bit<32> idx; +} + +struct headers { + ethernet_t eth_hdr; + H h; +} + +enum bit<2> MeterColor { + GREEN = 0, + YELLOW = 1, + RED = 2 +} + +struct Meta {} + +parser p(packet_in pkt, out headers hdr, inout Meta m, inout standard_metadata_t sm) { + state start { + transition parse_hdrs; + } + state parse_hdrs { + pkt.extract(hdr.eth_hdr); + pkt.extract(hdr.h); + transition accept; + } +} + +control ingress(inout headers h, inout Meta m, inout standard_metadata_t sm) { + meter(1024, MeterType.bytes) simple_meter; + apply { + MeterColor color; + simple_meter.execute_meter(0, color); + if (color == MeterColor.RED) { + h.h.color_status = 2; + } else if (color == MeterColor.YELLOW) { + h.h.color_status = 1; + } else { + h.h.color_status = 0; + } + simple_meter.execute_meter(1, color); + if (color == MeterColor.RED) { + h.h.color_status_2 = 2; + } else if (color == MeterColor.YELLOW) { + h.h.color_status_2 = 1; + } else { + h.h.color_status_2 = 0; + } + } +} + +control vrfy(inout headers h, inout Meta m) { apply {} } + +control update(inout headers h, inout Meta m) { apply {} } + +control egress(inout headers h, inout Meta m, inout standard_metadata_t sm) { apply {} } + +control deparser(packet_out pkt, in headers h) { + apply { + pkt.emit(h); + } +} +V1Switch(p(), vrfy(), ingress(), egress(), update(), deparser()) main; diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test_backend.cpp b/backends/p4tools/modules/testgen/targets/bmv2/test_backend.cpp index 32376bdb7b..3d17046ec2 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/test_backend.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/test_backend.cpp @@ -155,6 +155,14 @@ const TestSpec *Bmv2TestBackend::createTestSpec(const ExecutionState *executionS testSpec->addTestObject("clone_specs", sessionId, evaluatedInfo); } + const auto meterInfos = executionState->getTestObjectCategory("meter_values"); + for (const auto &testObject : meterInfos) { + const auto meterName = testObject.first; + const auto *meterInfo = testObject.second->checkedTo(); + const auto *evaluateMeterValue = meterInfo->evaluate(*completedModel); + testSpec->addTestObject("meter_values", meterName, evaluateMeterValue); + } + return testSpec; } diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test_spec.cpp b/backends/p4tools/modules/testgen/targets/bmv2/test_spec.cpp index 7614701890..98c93195a1 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/test_spec.cpp +++ b/backends/p4tools/modules/testgen/targets/bmv2/test_spec.cpp @@ -8,73 +8,121 @@ namespace P4Tools::P4Testgen::Bmv2 { /* ========================================================================================= - * Bmv2Register + * IndexExpression * ========================================================================================= */ -Bmv2RegisterValue::Bmv2RegisterValue(const IR::Expression *initialValue) - : initialValue(initialValue) {} +IndexExpression::IndexExpression(const IR::Expression *index, const IR::Expression *value) + : index(index), value(value) {} -void Bmv2RegisterValue::addRegisterCondition(Bmv2RegisterCondition cond) { - registerConditions.push_back(cond); +const IR::Constant *IndexExpression::getEvaluatedValue() const { + const auto *constant = value->to(); + BUG_CHECK(constant, "Variable is not a constant, has the test object %1% been evaluated?", + getObjectName()); + return constant; } -const IR::Expression *Bmv2RegisterValue::getInitialValue() const { return initialValue; } +const IR::Constant *IndexExpression::getEvaluatedIndex() const { + const auto *constant = index->to(); + BUG_CHECK(constant, "Variable is not a constant, has the test object %1% been evaluated?", + getObjectName()); + return constant; +} + +const IR::Expression *IndexExpression::getIndex() const { return index; } + +const IR::Expression *IndexExpression::getValue() const { return value; } + +cstring IndexExpression::getObjectName() const { return "IndexExpression"; } + +const IndexExpression *IndexExpression::evaluate(const Model &model) const { + const auto *evaluatedIndex = model.evaluate(index); + const auto *evaluatedValue = model.evaluate(value); + return new IndexExpression(evaluatedIndex, evaluatedValue); +} + +std::map> IndexMap::unravelMap() const { + std::map> valueMap; + for (auto it = indexConditions.rbegin(); it != indexConditions.rend(); ++it) { + const auto *storedIndex = it->getEvaluatedIndex(); + const auto *storedVal = it->getEvaluatedValue(); + // Important, if the element already exists in the map, ignore it. + // That index has been overwritten. + valueMap.insert({storedIndex->value, {storedIndex->type->width_bits(), storedVal}}); + } + return valueMap; +} + +/* ========================================================================================= + * Bmv2Register + * ========================================================================================= */ -cstring Bmv2RegisterValue::getObjectName() const { return "Bmv2RegisterValue"; } +IndexMap::IndexMap(const IR::Expression *initialValue) : initialValue(initialValue) {} -const IR::Expression *Bmv2RegisterValue::getCurrentValue(const IR::Expression *index) const { +void IndexMap::writeToIndex(const IR::Expression *index, const IR::Expression *value) { + indexConditions.emplace_back(index, value); +} + +const IR::Expression *IndexMap::getInitialValue() const { return initialValue; } + +const IR::Expression *IndexMap::getValueAtIndex(const IR::Expression *index) const { const IR::Expression *baseExpr = initialValue; - for (const auto &bmv2registerValue : registerConditions) { - const auto *storedIndex = bmv2registerValue.index; - const auto *storedVal = bmv2registerValue.value; + for (const auto &indexMap : indexConditions) { + const auto *storedIndex = indexMap.getIndex(); + const auto *storedVal = indexMap.getValue(); baseExpr = new IR::Mux(baseExpr->type, new IR::Equ(storedIndex, index), storedVal, baseExpr); } return baseExpr; } -const IR::Constant *Bmv2RegisterValue::getEvaluatedValue() const { +const IR::Constant *IndexMap::getEvaluatedInitialValue() const { const auto *constant = initialValue->to(); BUG_CHECK(constant, "Variable is not a constant, has the test object %1% been evaluated?", getObjectName()); return constant; } -const Bmv2RegisterValue *Bmv2RegisterValue::evaluate(const Model &model) const { - const auto *evaluatedValue = model.evaluate(initialValue); - auto *evaluatedRegisterValue = new Bmv2RegisterValue(evaluatedValue); - const std::vector evaluatedConditions; - for (const auto &cond : registerConditions) { - evaluatedRegisterValue->addRegisterCondition(*cond.evaluate(model)); +/* ========================================================================================= + * Bmv2V1ModelRegisterValue + * ========================================================================================= */ + +Bmv2V1ModelRegisterValue::Bmv2V1ModelRegisterValue(const IR::Expression *initialValue) + : IndexMap(initialValue) {} + +cstring Bmv2V1ModelRegisterValue::getObjectName() const { return "Bmv2V1ModelRegisterValue"; } + +const Bmv2V1ModelRegisterValue *Bmv2V1ModelRegisterValue::evaluate(const Model &model) const { + const auto *evaluatedValue = model.evaluate(getInitialValue()); + auto *evaluatedRegisterValue = new Bmv2V1ModelRegisterValue(evaluatedValue); + for (const auto &cond : indexConditions) { + const auto *evaluatedCond = cond.evaluate(model); + evaluatedRegisterValue->writeToIndex(evaluatedCond->getEvaluatedIndex(), + evaluatedCond->getEvaluatedValue()); } return evaluatedRegisterValue; } -Bmv2RegisterCondition::Bmv2RegisterCondition(const IR::Expression *index, - const IR::Expression *value) - : index(index), value(value) {} +/* ========================================================================================= + * Bmv2V1ModelMeterValue + * ========================================================================================= */ -const IR::Constant *Bmv2RegisterCondition::getEvaluatedValue() const { - const auto *constant = value->to(); - BUG_CHECK(constant, "Variable is not a constant, has the test object %1% been evaluated?", - getObjectName()); - return constant; -} +Bmv2V1ModelMeterValue::Bmv2V1ModelMeterValue(const IR::Expression *initialValue, bool isDirect) + : IndexMap(initialValue), isDirect(isDirect) {} -const IR::Constant *Bmv2RegisterCondition::getEvaluatedIndex() const { - const auto *constant = index->to(); - BUG_CHECK(constant, "Variable is not a constant, has the test object %1% been evaluated?", - getObjectName()); - return constant; -} +cstring Bmv2V1ModelMeterValue::getObjectName() const { return "Bmv2V1ModelMeterValue"; } -const Bmv2RegisterCondition *Bmv2RegisterCondition::evaluate(const Model &model) const { - const auto *evaluatedIndex = model.evaluate(index); - const auto *evaluatedValue = model.evaluate(value); - return new Bmv2RegisterCondition(evaluatedIndex, evaluatedValue); +const Bmv2V1ModelMeterValue *Bmv2V1ModelMeterValue::evaluate(const Model &model) const { + const auto *evaluatedValue = model.evaluate(getInitialValue()); + auto *evaluatedMeterValue = new Bmv2V1ModelMeterValue(evaluatedValue, isDirect); + for (const auto &cond : indexConditions) { + const auto *evaluatedCond = cond.evaluate(model); + evaluatedMeterValue->writeToIndex(evaluatedCond->getEvaluatedIndex(), + evaluatedCond->getEvaluatedValue()); + } + return evaluatedMeterValue; } -cstring Bmv2RegisterCondition::getObjectName() const { return "Bmv2RegisterCondition"; } +bool Bmv2V1ModelMeterValue::isDirectMeter() const { return isDirect; } /* ========================================================================================= * Bmv2V1ModelActionProfile diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test_spec.h b/backends/p4tools/modules/testgen/targets/bmv2/test_spec.h index 197553caa1..b301163bbb 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/test_spec.h +++ b/backends/p4tools/modules/testgen/targets/bmv2/test_spec.h @@ -21,68 +21,112 @@ namespace P4Tools::P4Testgen::Bmv2 { /* ========================================================================================= - * Bmv2Register + * IndexExpression * ========================================================================================= */ - -class Bmv2RegisterCondition : public TestObject { - public: - /// The register index. +/// Associates an expression with a particular index. +/// This object is used by all extern object that depend on a particular index to retrieve a value. +/// Examples are registers, meters, or counters. +class IndexExpression : public TestObject { + private: + /// The index of the expression. const IR::Expression *index; - /// The register value. + /// The value of the expression. const IR::Expression *value; - explicit Bmv2RegisterCondition(const IR::Expression *index, const IR::Expression *value); + public: + explicit IndexExpression(const IR::Expression *index, const IR::Expression *value); - /// @returns the evaluated register index. This means it must be a constant. + /// @returns the evaluated expression index. This means it must be a constant. /// The function will throw a bug if this is not the case. [[nodiscard]] const IR::Constant *getEvaluatedIndex() const; - /// @returns the evaluated condition of the register. This means it must be a constant. + /// @returns the evaluated condition of the expression. This means it must be a constant. /// The function will throw a bug if this is not the case. [[nodiscard]] const IR::Constant *getEvaluatedValue() const; - [[nodiscard]] const Bmv2RegisterCondition *evaluate(const Model &model) const override; + /// @returns the index stored in the index expression. + [[nodiscard]] const IR::Expression *getIndex() const; + + /// @returns the value stored in the index expression. + [[nodiscard]] const IR::Expression *getValue() const; + + [[nodiscard]] const IndexExpression *evaluate(const Model &model) const override; [[nodiscard]] cstring getObjectName() const override; }; -/// This object tracks the list of writes that have been performed to a particular register. The -/// registerConditionList represents the pair of the index that was written, and the value that -/// was written to this index. When reading from a register, we can furl this list starting -/// from the first index into a set of nested Mux expressions (e.g., "readIndex == savedIndex ? -/// savedValue : defaultValue", where defaultValue may be another Mux expression). If the read index -/// matches with the index that was saved in this tuple, we return the value, otherwise we unroll -/// the nested MUX expressions. This implicitly handles overwrites too, as the latest writes to a -/// particular index appear the earliest in this unraveling phase.. -class Bmv2RegisterValue : public TestObject { - private: - /// A new Bmv2RegisterValue always requires an initial value. This can be a constant or taint. +/* ========================================================================================= + * IndexMap + * ========================================================================================= */ +/// Readable and writable symbolic map, which maps indices to particular values. +class IndexMap : public TestObject { + protected: + /// A new IndexMap always requires an initial value. This can be a constant or taint. const IR::Expression *initialValue; /// Each element is an API name paired with a match rule. - std::vector registerConditions; + std::vector indexConditions; public: - explicit Bmv2RegisterValue(const IR::Expression *initialValue); + explicit IndexMap(const IR::Expression *initialValue); - [[nodiscard]] cstring getObjectName() const override; - - /// Each element is an API name paired with a match rule. - void addRegisterCondition(Bmv2RegisterCondition cond); + /// Write @param value to @param index. + void writeToIndex(const IR::Expression *index, const IR::Expression *value); /// @returns the value with which this register has been initialized. [[nodiscard]] const IR::Expression *getInitialValue() const; /// @returns the current value of this register after writes have been performed according to a /// provided index. - const IR::Expression *getCurrentValue(const IR::Expression *index) const; + [[nodiscard]] const IR::Expression *getValueAtIndex(const IR::Expression *index) const; /// @returns the evaluated register value. This means it must be a constant. /// The function will throw a bug if this is not the case. - [[nodiscard]] const IR::Constant *getEvaluatedValue() const; + [[nodiscard]] const IR::Constant *getEvaluatedInitialValue() const; + + /// Return the "writes" to this index map as a + [[nodiscard]] std::map> unravelMap() const; +}; + +/* ========================================================================================= + * Bmv2V1ModelRegisterValue + * ========================================================================================= */ +/// This object tracks the list of writes that have been performed to a particular register. The +/// registerConditionList represents the pair of the index that was written, and the value that +/// was written to this index. When reading from a register, we can furl this list starting +/// from the first index into a set of nested Mux expressions (e.g., "readIndex == savedIndex ? +/// savedValue : defaultValue", where defaultValue may be another Mux expression). If the read +/// index matches with the index that was saved in this tuple, we return the value, otherwise we +/// unroll the nested MUX expressions. This implicitly handles overwrites too, as the latest +/// writes to a particular index appear the earliest in this unraveling phase.. +class Bmv2V1ModelRegisterValue : public IndexMap { + public: + explicit Bmv2V1ModelRegisterValue(const IR::Expression *initialValue); + + [[nodiscard]] cstring getObjectName() const override; + + [[nodiscard]] const Bmv2V1ModelRegisterValue *evaluate(const Model &model) const override; +}; + +/* ========================================================================================= + * Bmv2V1ModelMeterValue + * ========================================================================================= */ + +class Bmv2V1ModelMeterValue : public IndexMap { + private: + /// Whether the meter is a direct meter. + bool isDirect; + + public: + explicit Bmv2V1ModelMeterValue(const IR::Expression *initialValue, bool isDirect); + + [[nodiscard]] cstring getObjectName() const override; + + [[nodiscard]] const Bmv2V1ModelMeterValue *evaluate(const Model &model) const override; - [[nodiscard]] const Bmv2RegisterValue *evaluate(const Model &model) const override; + /// @returns whether the meter associated with this meter value object is a direct meter. + [[nodiscard]] bool isDirectMeter() const; }; /* ========================================================================================= @@ -193,8 +237,8 @@ class Bmv2V1ModelCloneSpec : public TestObject { /// The cloned packet will be emitted on this port. const IR::Expression *clonePort; - /// Whether this clone information is associated with the cloned packet (true) or the regular - /// packet (false). + /// Whether this clone information is associated with the cloned packet (true) or the + /// regular packet (false). bool isClone; public: @@ -211,7 +255,8 @@ class Bmv2V1ModelCloneSpec : public TestObject { /// @returns the clone port expression. [[nodiscard]] const IR::Expression *getClonePort() const; - /// @returns information whether we are dealing with the packet clone or the real output packet. + /// @returns information whether we are dealing with the packet clone or the real output + /// packet. [[nodiscard]] bool isClonedPacket() const; /// @returns the evaluated clone port. This means it must be a constant. diff --git a/backends/p4tools/modules/testgen/targets/pna/backend/metadata/metadata.cpp b/backends/p4tools/modules/testgen/targets/pna/backend/metadata/metadata.cpp index 0f8120245c..fa4694bf57 100644 --- a/backends/p4tools/modules/testgen/targets/pna/backend/metadata/metadata.cpp +++ b/backends/p4tools/modules/testgen/targets/pna/backend/metadata/metadata.cpp @@ -66,7 +66,7 @@ inja::json Metadata::getVerify(const TestSpec *testSpec) { inja::json verifyData = inja::json::object(); auto egressPacket = testSpec->getEgressPacket(); if (egressPacket.has_value()) { - const auto *const packet = egressPacket.value(); + const auto *packet = egressPacket.value(); verifyData["eg_port"] = packet->getPort(); const auto *payload = packet->getEvaluatedPayload(); const auto *payloadMask = packet->getEvaluatedPayloadMask(); @@ -94,7 +94,7 @@ statement_coverage: {{coverage}} ## endfor # The input packet in hexadecimal. -input_packet: \"{{send.pkt}}\" +input_packet: "{{send.pkt}}" # Parsed headers and their offsets. header_offsets: @@ -105,7 +105,7 @@ input_packet: \"{{send.pkt}}\" # Metadata results after this test has completed. metadata: ## for metadata_field in metadata_fields - {{metadata_field.name}}: [value: \"{{metadata_field.value}}\", offset: {{metadata_field.offset}}] + {{metadata_field.name}}: [value: "{{metadata_field.value}}", offset: {{metadata_field.offset}}] ## endfor )"""); return TEST_CASE;