diff --git a/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp b/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp index 9c6df46fa9..d9b612d9e6 100644 --- a/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp +++ b/backends/p4tools/modules/testgen/core/small_step/expr_stepper.cpp @@ -96,6 +96,7 @@ bool ExprStepper::preorder(const IR::ArrayIndex *arrIndex) { } void ExprStepper::evalActionCall(const IR::P4Action *action, const IR::MethodCallExpression *call) { + state.markVisited(action); const auto *actionNameSpace = action->to(); BUG_CHECK(actionNameSpace, "Does not instantiate an INamespace: %1%", actionNameSpace); // If the action has arguments, these are usually directionless control plane input. diff --git a/backends/p4tools/modules/testgen/core/small_step/expr_stepper.h b/backends/p4tools/modules/testgen/core/small_step/expr_stepper.h index 0c197bf78e..78930a828f 100644 --- a/backends/p4tools/modules/testgen/core/small_step/expr_stepper.h +++ b/backends/p4tools/modules/testgen/core/small_step/expr_stepper.h @@ -103,7 +103,8 @@ class ExprStepper : public AbstractStepper { const IR::Vector *args, const ExecutionState &state); - /// Evaluates a call to an action. This usually only happens when a table is invoked. + /// Evaluates a call to an action. This usually only happens when a table is invoked or when + /// action is directly invoked from a control. /// In other cases, actions should be inlined. When the action call is evaluated, we use /// symbolic variables to pass arguments across execution boundaries. These variables persist /// until the end of program execution. diff --git a/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.cpp b/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.cpp index 7d6e0e8e03..4e15bc1877 100644 --- a/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.cpp +++ b/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.cpp @@ -80,7 +80,7 @@ bool CoverableNodesScanner::preorder(const IR::MethodCallExpression *call) { const auto *tableAction = entry->action->checkedTo(); const auto *actionType = executionState.getP4Action(tableAction); - actionType->body->apply_visitor_preorder(*this); + actionType->apply_visitor_preorder(*this); } } } else { @@ -88,19 +88,19 @@ bool CoverableNodesScanner::preorder(const IR::MethodCallExpression *call) { const auto *tableAction = action->expression->checkedTo(); const auto *actionType = executionState.getP4Action(tableAction); - actionType->body->apply_visitor_preorder(*this); + actionType->apply_visitor_preorder(*this); } } const auto *defaultAction = table->getDefaultAction(); const auto *tableAction = defaultAction->checkedTo(); const auto *actionType = executionState.getP4Action(tableAction); - actionType->body->apply_visitor_preorder(*this); + actionType->apply_visitor_preorder(*this); } return false; } if (call->method->type->is()) { const auto *actionType = executionState.getP4Action(call); - actionType->body->apply_visitor_preorder(*this); + actionType->apply_visitor_preorder(*this); return false; } return false; @@ -122,6 +122,16 @@ bool CoverableNodesScanner::preorder(const IR::ExitStatement *stmt) { return true; } +bool CoverableNodesScanner::preorder(const IR::P4Action *act) { + // Only track actions, which have a valid source position in the P4 program. + if (coverageOptions.coverActions && act->getSourceInfo().isValid()) { + coverableNodes.insert(act); + } + // Visit body only. + act->body->apply_visitor_preorder(*this); + return false; +} + const P4::Coverage::CoverageSet &CoverableNodesScanner::getCoverableNodes() { return coverableNodes; } diff --git a/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.h b/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.h index ed31c28410..1a2005615c 100644 --- a/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.h +++ b/backends/p4tools/modules/testgen/lib/collect_coverable_nodes.h @@ -39,6 +39,9 @@ class CoverableNodesScanner : public Inspector { bool preorder(const IR::ExitStatement *stmt) override; bool preorder(const IR::MethodCallExpression *call) override; + /// Actions coverage. + bool preorder(const IR::P4Action *act) override; + public: explicit CoverableNodesScanner(const ExecutionState &state); diff --git a/backends/p4tools/modules/testgen/lib/execution_state.cpp b/backends/p4tools/modules/testgen/lib/execution_state.cpp index 5f531146e8..0b3996e4e2 100644 --- a/backends/p4tools/modules/testgen/lib/execution_state.cpp +++ b/backends/p4tools/modules/testgen/lib/execution_state.cpp @@ -169,6 +169,10 @@ void ExecutionState::markVisited(const IR::Node *node) { if (node->is() && !coverageOptions.coverTableEntries) { return; } + // Do not add actions, if coverActions is not toggled. + if (node->is() && !coverageOptions.coverActions) { + return; + } visitedNodes.emplace(node); } diff --git a/backends/p4tools/modules/testgen/options.cpp b/backends/p4tools/modules/testgen/options.cpp index c328335efe..a13d0b031b 100644 --- a/backends/p4tools/modules/testgen/options.cpp +++ b/backends/p4tools/modules/testgen/options.cpp @@ -255,10 +255,8 @@ TestgenOptions::TestgenOptions() registerOption( "--track-coverage", "coverageItem", [this](const char *arg) { - static std::set const COVERAGE_OPTIONS = { - "STATEMENTS", - "TABLE_ENTRIES", - }; + static std::set const COVERAGE_OPTIONS = {"STATEMENTS", "TABLE_ENTRIES", + "ACTIONS"}; hasCoverageTracking = true; auto selectionString = cstring(arg).toUpper(); auto it = COVERAGE_OPTIONS.find(selectionString); @@ -271,6 +269,10 @@ TestgenOptions::TestgenOptions() coverageOptions.coverTableEntries = true; return true; } + if (selectionString == "ACTIONS") { + coverageOptions.coverActions = true; + return true; + } } ::error( "Coverage tracking for label %1% not supported. Supported coverage tracking " @@ -280,7 +282,8 @@ TestgenOptions::TestgenOptions() return false; }, "Specifies, which IR nodes to track for coverage in the targeted P4 program. Multiple " - "options are possible: Currently supported: STATEMENTS, TABLE_ENTRIES " + "options are possible: Currently supported: STATEMENTS, TABLE_ENTRIES (table rules encoded " + "in the table entries in P4), ACTIONS (actions invoked, directly or by tables). " "Defaults to no coverage."); registerOption( diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test/P4Tests.cmake b/backends/p4tools/modules/testgen/targets/bmv2/test/P4Tests.cmake index daf30acfb2..52b5b371ec 100644 --- a/backends/p4tools/modules/testgen/targets/bmv2/test/P4Tests.cmake +++ b/backends/p4tools/modules/testgen/targets/bmv2/test/P4Tests.cmake @@ -33,7 +33,7 @@ option(P4TOOLS_TESTGEN_BMV2_TEST_PROTOBUF_IR "Run tests on the Protobuf test bac option(P4TOOLS_TESTGEN_BMV2_TEST_PTF "Run tests on the PTF test back end" ON) option(P4TOOLS_TESTGEN_BMV2_TEST_STF "Run tests on the STF test back end" ON) # Test settings. -set(EXTRA_OPTS "--strict --print-traces --seed 1000 --max-tests 10 ") +set(EXTRA_OPTS "--strict --print-traces --seed 1000 --max-tests 10 --track-coverage STATEMENTS --track-coverage TABLE_ENTRIES --track-coverage ACTIONS ") # ASSERT_ASSUME TESTS include(${CMAKE_CURRENT_LIST_DIR}/AssumeAssertTests.cmake) diff --git a/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_table_actions_coverage.p4 b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_table_actions_coverage.p4 new file mode 100644 index 0000000000..eaeafa5a5b --- /dev/null +++ b/backends/p4tools/modules/testgen/targets/bmv2/test/p4-programs/bmv2_table_actions_coverage.p4 @@ -0,0 +1,115 @@ +#include + +header ethernet_t { + bit<48> dst_addr; + bit<48> src_addr; + bit<16> eth_type; +} + +header H { + bit<8> a; + bit<8> b; +} + +struct Headers { + ethernet_t eth_hdr; + H h; +} + +struct Meta {} + +parser p(packet_in pkt, out Headers h, inout Meta meta, inout standard_metadata_t stdmeta) { + state start { + pkt.extract(h.eth_hdr); + transition parse_h; + } + state parse_h { + pkt.extract(h.h); + transition accept; + } +} + +control vrfy(inout Headers h, inout Meta meta) { + apply { } +} + +control ingress(inout Headers h, inout Meta m, inout standard_metadata_t s) { + + action t0_a0() { /* empty */ } + action t0_a1() { /* empty */ } + action t0_a2() { h.h.a = 4; } + + table t0 { + actions = { t0_a0; t0_a1; t0_a2; } + size = 8; + default_action = t0_a0(); + } + + action t1_a0() { /* empty */ } + action t1_a1() { /* empty */ } + action t1_a2() { h.h.a = h.h.a + 7; } + + table t1 { + key = { h.h.a : exact; } + actions = { t1_a0; t1_a1; t1_a2; } + size = 8; + const default_action = t1_a1(); + } + + action t2_a0() { /* empty */ } + action t2_a1() { /* empty */ } + action t2_a2() { h.h.a = h.h.a + 13; } + + table t2 { + key = { h.h.a : exact; } + actions = { t2_a0; t2_a1; t2_a2; } + size = 8; + const entries = { + 0 : t2_a2; + 1 : t2_a2; + 2 : t2_a0; + 3 : t2_a1; + // implicit default NoAction + } + } + + action free_a0() { /* empty */ } + action free_a1() { /* empty */ } + action free_nested() { /* empty */ } + action free_a2() { h.h.a = h.h.a + 11; free_nested(); } + + apply { + if (h.h.isValid()) { + // check action coverage tracking on default actions + t0.apply(); + // check action coverage tracking on non-default actions + t1.apply(); + // check action coverage tracking on const entries + t2.apply(); + // check action coverage on freestanding actions + if (h.h.a > 8) { + free_a0(); + } else if (h.h.b > 8) { + free_a1(); + } else { + free_a2(); + } + } + } +} + +control egress(inout Headers h, inout Meta m, inout standard_metadata_t s) { + apply { } +} + +control update(inout Headers h, inout Meta m) { + apply { } +} + +control deparser(packet_out pkt, in Headers h) { + apply { + pkt.emit(h); + } +} + +V1Switch(p(), vrfy(), ingress(), egress(), update(), deparser()) main; diff --git a/midend/coverage.cpp b/midend/coverage.cpp index 50f4159710..ccc204466e 100644 --- a/midend/coverage.cpp +++ b/midend/coverage.cpp @@ -48,6 +48,14 @@ bool CollectNodes::preorder(const IR::ExitStatement *stmt) { return true; } +bool CollectNodes::preorder(const IR::P4Action *act) { + // Only track actions, which have a valid source position in the P4 program. + if (coverageOptions.coverActions && act->getSourceInfo().isValid()) { + coverableNodes.insert(act); + } + return true; +} + void printCoverageReport(const CoverageSet &all, const CoverageSet &visited) { if (all.empty()) { return; diff --git a/midend/coverage.h b/midend/coverage.h index 58038abe5e..aa0a108191 100644 --- a/midend/coverage.h +++ b/midend/coverage.h @@ -27,6 +27,9 @@ struct CoverageOptions { bool coverStatements = false; /// Cover IR::Entry bool coverTableEntries = false; + /// Cover IR::P4Action + bool coverActions = false; + /// Skip tests which do not increase coverage. bool onlyCoveringTests = false; }; @@ -52,6 +55,9 @@ class CollectNodes : public Inspector { /// Table entry coverage. bool preorder(const IR::Entry *entry) override; + /// Actions coverage. + bool preorder(const IR::P4Action *act) override; + public: explicit CollectNodes(CoverageOptions coverageOptions);