|
| 1 | +#include "backends/p4tools/modules/testgen/targets/pna/backend/stf/stf.h" |
| 2 | + |
| 3 | +#include <filesystem> |
| 4 | +#include <fstream> |
| 5 | +#include <iomanip> |
| 6 | +#include <list> |
| 7 | +#include <map> |
| 8 | +#include <optional> |
| 9 | +#include <regex> // NOLINT |
| 10 | +#include <string> |
| 11 | +#include <utility> |
| 12 | +#include <vector> |
| 13 | + |
| 14 | +#include <boost/multiprecision/cpp_int.hpp> |
| 15 | +#include <boost/multiprecision/detail/et_ops.hpp> |
| 16 | +#include <boost/multiprecision/number.hpp> |
| 17 | +#include <boost/multiprecision/traits/explicit_conversion.hpp> |
| 18 | +#include <inja/inja.hpp> |
| 19 | + |
| 20 | +#include "backends/p4tools/common/lib/format_int.h" |
| 21 | +#include "backends/p4tools/common/lib/util.h" |
| 22 | +#include "ir/ir.h" |
| 23 | +#include "ir/irutils.h" |
| 24 | +#include "lib/exceptions.h" |
| 25 | +#include "lib/log.h" |
| 26 | +#include "nlohmann/json.hpp" |
| 27 | + |
| 28 | +#include "backends/p4tools/modules/testgen/lib/exceptions.h" |
| 29 | +#include "backends/p4tools/modules/testgen/lib/test_framework.h" |
| 30 | +#include "backends/p4tools/modules/testgen/lib/test_object.h" |
| 31 | + |
| 32 | +namespace P4::P4Tools::P4Testgen::Pna { |
| 33 | + |
| 34 | +STF::STF(const TestBackendConfiguration &testBackendConfiguration) |
| 35 | + : TestFramework(testBackendConfiguration) {} |
| 36 | + |
| 37 | +inja::json STF::getControlPlane(const TestSpec *testSpec) { |
| 38 | + inja::json controlPlaneJson = inja::json::object(); |
| 39 | + |
| 40 | + // Map of actionProfiles and actionSelectors for easy reference. |
| 41 | + std::map<cstring, cstring> apAsMap; |
| 42 | + |
| 43 | + auto tables = testSpec->getTestObjectCategory("tables"_cs); |
| 44 | + if (!tables.empty()) { |
| 45 | + controlPlaneJson["tables"] = inja::json::array(); |
| 46 | + } |
| 47 | + for (const auto &testObject : tables) { |
| 48 | + inja::json tblJson; |
| 49 | + tblJson["table_name"] = testObject.first.c_str(); |
| 50 | + const auto *const tblConfig = testObject.second->checkedTo<TableConfig>(); |
| 51 | + const auto *tblRules = tblConfig->getRules(); |
| 52 | + tblJson["rules"] = inja::json::array(); |
| 53 | + for (const auto &tblRule : *tblRules) { |
| 54 | + inja::json rule; |
| 55 | + const auto *matches = tblRule.getMatches(); |
| 56 | + const auto *actionCall = tblRule.getActionCall(); |
| 57 | + const auto *actionArgs = actionCall->getArgs(); |
| 58 | + rule["action_name"] = actionCall->getActionName().c_str(); |
| 59 | + auto j = getControlPlaneForTable(*matches, *actionArgs); |
| 60 | + rule["rules"] = std::move(j); |
| 61 | + rule["priority"] = tblRule.getPriority(); |
| 62 | + tblJson["rules"].push_back(rule); |
| 63 | + } |
| 64 | + |
| 65 | + controlPlaneJson["tables"].push_back(tblJson); |
| 66 | + } |
| 67 | + |
| 68 | + return controlPlaneJson; |
| 69 | +} |
| 70 | + |
| 71 | +inja::json STF::getControlPlaneForTable(const TableMatchMap &matches, |
| 72 | + const std::vector<ActionArg> &args) { |
| 73 | + inja::json rulesJson; |
| 74 | + |
| 75 | + rulesJson["matches"] = inja::json::array(); |
| 76 | + rulesJson["act_args"] = inja::json::array(); |
| 77 | + rulesJson["needs_priority"] = false; |
| 78 | + |
| 79 | + for (const auto &match : matches) { |
| 80 | + auto fieldName = match.first; |
| 81 | + const auto &fieldMatch = match.second; |
| 82 | + |
| 83 | + // Replace header stack indices hdr[<index>] with hdr$<index>. |
| 84 | + // TODO: This is a limitation of the stf parser. We should fix this. |
| 85 | + std::regex hdrStackRegex(R"(\[([0-9]+)\])"); |
| 86 | + auto indexName = std::regex_replace(fieldName.c_str(), hdrStackRegex, "$$$1"); |
| 87 | + if (indexName != fieldName.c_str()) { |
| 88 | + fieldName = cstring::to_cstring(indexName); |
| 89 | + } |
| 90 | + |
| 91 | + inja::json j; |
| 92 | + j["field_name"] = fieldName; |
| 93 | + if (const auto *elem = fieldMatch->to<Exact>()) { |
| 94 | + j["value"] = formatHexExpr(elem->getEvaluatedValue()); |
| 95 | + } else if (const auto *elem = fieldMatch->to<Ternary>()) { |
| 96 | + const auto *dataValue = elem->getEvaluatedValue(); |
| 97 | + const auto *maskField = elem->getEvaluatedMask(); |
| 98 | + BUG_CHECK(dataValue->type->width_bits() == maskField->type->width_bits(), |
| 99 | + "Data value and its mask should have the same bit width."); |
| 100 | + // Using the width from mask - should be same as data |
| 101 | + auto dataStr = formatBinExpr(dataValue, {false, true, false}); |
| 102 | + auto maskStr = formatBinExpr(maskField, {false, true, false}); |
| 103 | + std::string data = "0b"; |
| 104 | + for (size_t dataPos = 0; dataPos < dataStr.size(); ++dataPos) { |
| 105 | + if (maskStr.at(dataPos) == '0') { |
| 106 | + data += "*"; |
| 107 | + } else { |
| 108 | + data += dataStr.at(dataPos); |
| 109 | + } |
| 110 | + } |
| 111 | + j["value"] = data; |
| 112 | + // If the rule has a ternary match we need to add the priority. |
| 113 | + rulesJson["needs_priority"] = true; |
| 114 | + } else if (const auto *elem = fieldMatch->to<LPM>()) { |
| 115 | + const auto *dataValue = elem->getEvaluatedValue(); |
| 116 | + auto prefixLen = elem->getEvaluatedPrefixLength()->asInt(); |
| 117 | + auto fieldWidth = dataValue->type->width_bits(); |
| 118 | + auto maxVal = IR::getMaxBvVal(prefixLen); |
| 119 | + const auto *maskField = |
| 120 | + IR::Constant::get(dataValue->type, maxVal << (fieldWidth - prefixLen)); |
| 121 | + BUG_CHECK(dataValue->type->width_bits() == maskField->type->width_bits(), |
| 122 | + "Data value and its mask should have the same bit width."); |
| 123 | + // Using the width from mask - should be same as data |
| 124 | + auto dataStr = formatBinExpr(dataValue, {false, true, false}); |
| 125 | + auto maskStr = formatBinExpr(maskField, {false, true, false}); |
| 126 | + std::string data = "0b"; |
| 127 | + for (size_t dataPos = 0; dataPos < dataStr.size(); ++dataPos) { |
| 128 | + if (maskStr.at(dataPos) == '0') { |
| 129 | + data += "*"; |
| 130 | + } else { |
| 131 | + data += dataStr.at(dataPos); |
| 132 | + } |
| 133 | + } |
| 134 | + j["value"] = data; |
| 135 | + // If the rule has a ternary match we need to add the priority. |
| 136 | + rulesJson["needs_priority"] = true; |
| 137 | + } else { |
| 138 | + TESTGEN_UNIMPLEMENTED("Unsupported table key match type \"%1%\"", |
| 139 | + fieldMatch->getObjectName()); |
| 140 | + } |
| 141 | + rulesJson["matches"].push_back(j); |
| 142 | + } |
| 143 | + |
| 144 | + for (const auto &actArg : args) { |
| 145 | + inja::json j; |
| 146 | + j["param"] = actArg.getActionParamName().c_str(); |
| 147 | + j["value"] = formatHexExpr(actArg.getEvaluatedValue()); |
| 148 | + rulesJson["act_args"].push_back(j); |
| 149 | + } |
| 150 | + |
| 151 | + return rulesJson; |
| 152 | +} |
| 153 | + |
| 154 | +inja::json STF::getSend(const TestSpec *testSpec) { |
| 155 | + const auto *iPacket = testSpec->getIngressPacket(); |
| 156 | + const auto *payload = iPacket->getEvaluatedPayload(); |
| 157 | + inja::json sendJson; |
| 158 | + sendJson["ig_port"] = iPacket->getPort(); |
| 159 | + auto dataStr = formatHexExpr(payload, {false, true, false}); |
| 160 | + sendJson["pkt"] = dataStr; |
| 161 | + sendJson["pkt_size"] = payload->type->width_bits(); |
| 162 | + return sendJson; |
| 163 | +} |
| 164 | + |
| 165 | +inja::json STF::getVerify(const TestSpec *testSpec) { |
| 166 | + inja::json verifyData = inja::json::object(); |
| 167 | + if (testSpec->getEgressPacket() != std::nullopt) { |
| 168 | + const auto &packet = **testSpec->getEgressPacket(); |
| 169 | + verifyData["eg_port"] = packet.getPort(); |
| 170 | + const auto *payload = packet.getEvaluatedPayload(); |
| 171 | + const auto *payloadMask = packet.getEvaluatedPayloadMask(); |
| 172 | + auto dataStr = formatHexExpr(payload, {false, true, false}); |
| 173 | + if (payloadMask != nullptr) { |
| 174 | + // If a mask is present, construct the packet data with wildcard `*` where there are |
| 175 | + // non zero nibbles |
| 176 | + auto maskStr = formatHexExpr(payloadMask, {false, true, false}); |
| 177 | + std::string packetData; |
| 178 | + for (size_t dataPos = 0; dataPos < dataStr.size(); ++dataPos) { |
| 179 | + if (maskStr.at(dataPos) != 'F') { |
| 180 | + // TODO: We are being conservative here and adding a wildcard for any 0 |
| 181 | + // in the 4b nibble |
| 182 | + packetData += "*"; |
| 183 | + } else { |
| 184 | + packetData += dataStr[dataPos]; |
| 185 | + } |
| 186 | + } |
| 187 | + verifyData["exp_pkt"] = packetData; |
| 188 | + } else { |
| 189 | + verifyData["exp_pkt"] = dataStr; |
| 190 | + } |
| 191 | + } |
| 192 | + return verifyData; |
| 193 | +} |
| 194 | + |
| 195 | +std::string STF::getTestCaseTemplate() { |
| 196 | + static std::string TEST_CASE( |
| 197 | + R"""(# p4testgen seed: {{ default(seed, "none") }} |
| 198 | +# Date generated: {{timestamp}} |
| 199 | +## if length(selected_branches) > 0 |
| 200 | + # {{selected_branches}} |
| 201 | +## endif |
| 202 | +# Current node coverage: {{coverage}} |
| 203 | +# Traces |
| 204 | +## for trace_item in trace |
| 205 | +# {{trace_item}} |
| 206 | +##endfor |
| 207 | +
|
| 208 | +## if control_plane |
| 209 | +## for table in control_plane.tables |
| 210 | +# Table {{table.table_name}} |
| 211 | +## for rule in table.rules |
| 212 | +add {{table.table_name}} {% if rule.rules.needs_priority %}{{rule.priority}} {% endif %}{% for r in rule.rules.matches %}{{r.field_name}}:{{r.value}} {% endfor %}{{rule.action_name}}({% for a in rule.rules.act_args %}{{a.param}}:{{a.value}}{% if not loop.is_last %},{% endif %}{% endfor %}) |
| 213 | +## endfor |
| 214 | +## endfor |
| 215 | +## endif |
| 216 | +
|
| 217 | +packet {{send.ig_port}} {{send.pkt}} |
| 218 | +## if verify |
| 219 | +expect {{verify.eg_port}} {{verify.exp_pkt}} |
| 220 | +## endif |
| 221 | +)"""); |
| 222 | + return TEST_CASE; |
| 223 | +} |
| 224 | + |
| 225 | +void STF::emitTestcase(const TestSpec *testSpec, cstring selectedBranches, size_t testId, |
| 226 | + const std::string &testCase, float currentCoverage) { |
| 227 | + inja::json dataJson; |
| 228 | + if (selectedBranches != nullptr) { |
| 229 | + dataJson["selected_branches"] = selectedBranches.c_str(); |
| 230 | + } |
| 231 | + |
| 232 | + auto optSeed = getTestBackendConfiguration().seed; |
| 233 | + if (optSeed.has_value()) { |
| 234 | + dataJson["seed"] = optSeed.value(); |
| 235 | + } |
| 236 | + dataJson["test_id"] = testId; |
| 237 | + dataJson["trace"] = getTrace(testSpec); |
| 238 | + dataJson["control_plane"] = getControlPlane(testSpec); |
| 239 | + dataJson["send"] = getSend(testSpec); |
| 240 | + dataJson["verify"] = getVerify(testSpec); |
| 241 | + dataJson["timestamp"] = Utils::getTimeStamp(); |
| 242 | + std::stringstream coverageStr; |
| 243 | + coverageStr << std::setprecision(2) << currentCoverage; |
| 244 | + dataJson["coverage"] = coverageStr.str(); |
| 245 | + |
| 246 | + LOG5("STF test back end: emitting testcase:" << std::setw(4) << dataJson); |
| 247 | + auto optBasePath = getTestBackendConfiguration().fileBasePath; |
| 248 | + BUG_CHECK(optBasePath.has_value(), "Base path is not set."); |
| 249 | + auto incrementedbasePath = optBasePath.value(); |
| 250 | + incrementedbasePath.concat("_" + std::to_string(testId)); |
| 251 | + incrementedbasePath.replace_extension(".stf"); |
| 252 | + auto stfFileStream = std::ofstream(incrementedbasePath); |
| 253 | + inja::render_to(stfFileStream, testCase, dataJson); |
| 254 | + stfFileStream.flush(); |
| 255 | +} |
| 256 | + |
| 257 | +void STF::writeTestToFile(const TestSpec *testSpec, cstring selectedBranches, size_t testId, |
| 258 | + float currentCoverage) { |
| 259 | + std::string testCase = getTestCaseTemplate(); |
| 260 | + emitTestcase(testSpec, selectedBranches, testId, testCase, currentCoverage); |
| 261 | +} |
| 262 | + |
| 263 | +} // namespace P4::P4Tools::P4Testgen::Pna |
0 commit comments