Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backends/p4tools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ To add a new target:
- CTest labels are defined per target under `modules/testgen/targets/*`.
- Benchmarks and plotting scripts live in `modules/testgen/benchmarks/`.

## P4Testgen Workflow Notes
- **Rebuild after code changes**: after editing `backends/p4tools/modules/testgen/**`, rebuild `p4testgen` explicitly (`cmake --build build --target p4testgen -j<N>`) before trusting test results.
- **Targeted eBPF test run**: use `ctest --test-dir build --output-on-failure -R testgen-p4c-ebpf/<name>.p4` for quick iteration.
- **Inspect generated tests**: testgen outputs live under `build/testgen/testgen-p4c-<target>/<program>.out/` and are often the fastest way to diagnose mismatches between symbolic traces and emitted control-plane commands.
- **Root-required eBPF tests**: some eBPF executions require root/network namespace setup; in non-root environments they may be skipped or fail for environment reasons rather than compiler logic.
- **Prefer non-root BMv2 PTF runs**: install `pynng` in the active environment and run BMv2 PTF tests with nanomsg mode (the testgen runner already passes `--use-nanomsg`), so `uv run ctest ...` works without sudo.

## P4Testgen Structure Notes
- **Where backend outputs are emitted**: each target backend converts `TestSpec` objects to target-specific artifacts under `modules/testgen/targets/<target>/` (for example STF, PTF, metadata, or protobuf emitters).

## Key Entry Points
- `README.md`: overview, dependencies, and layout.
- `modules/testgen/README.md`: P4Testgen usage, extensions, and limitations.
Expand Down
2 changes: 2 additions & 0 deletions backends/p4tools/modules/testgen/lib/test_framework.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class TestFramework {
if (apObject != nullptr) {
const auto *actionProfile = apObject->checkedTo<ProfileType>();
tblJson["has_ap"] = true;
tblJson["action_profile"] = actionProfile->getProfileDecl()->controlPlaneName();
// Check if we have an Action Selector too.
// TODO: Change this to check in ActionSelector with table
// property "action_selectors".
Expand All @@ -99,6 +100,7 @@ class TestFramework {
apAsMap[actionProfile->getProfileDecl()->controlPlaneName()] =
actionSelector->getSelectorDecl()->controlPlaneName();
tblJson["has_as"] = true;
tblJson["action_selector"] = actionSelector->getSelectorDecl()->controlPlaneName();
}
}
}
Expand Down
30 changes: 19 additions & 11 deletions backends/p4tools/modules/testgen/targets/bmv2/table_stepper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ bool Bmv2V1ModelTableStepper::checkForActionProfile() {
if (const auto *implCall = implExpr->expression->to<IR::ConstructorCallExpression>()) {
const auto *implDeclType = state->resolveType(implCall->constructedType);
implExtern = implDeclType->checkedTo<IR::Type_Extern>();
implDecl = implExtern;
implDecl = impl;
} else if (const auto *implPath = implExpr->expression->to<IR::PathExpression>()) {
const auto *declInst = state->findDecl(implPath)->checkedTo<IR::Declaration_Instance>();
const auto *implDeclType = state->resolveType(declInst->type);
Expand Down Expand Up @@ -324,7 +324,7 @@ bool Bmv2V1ModelTableStepper::checkForActionSelector() {
if (const auto *implCall = selectorExpr->expression->to<IR::ConstructorCallExpression>()) {
const auto *selectorDeclType = state->resolveType(implCall->constructedType);
selectorExtern = selectorDeclType->checkedTo<IR::Type_Extern>();
selectorDecl = selectorExtern;
selectorDecl = impl;
} else if (const auto *implPath = selectorExpr->expression->to<IR::PathExpression>()) {
const auto *declInst = state->findDecl(implPath)->checkedTo<IR::Declaration_Instance>();
const auto *selectorDeclType = state->resolveType(declInst->type);
Expand All @@ -338,18 +338,19 @@ bool Bmv2V1ModelTableStepper::checkForActionSelector() {
if (selectorExtern->name != "action_selector") {
return false;
}
// 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"_cs, selectorDecl->controlPlaneName(), false);
state->getTestObject("action_selector"_cs, 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.
// add the generated action selector and backing profile.
bmv2V1ModelProperties.addProfileToState = true;
bmv2V1ModelProperties.actionProfile = new Bmv2V1ModelActionProfile(selectorDecl);
bmv2V1ModelProperties.actionSelector =
new Bmv2V1ModelActionSelector(selectorDecl, bmv2V1ModelProperties.actionProfile);
return true;
}
bmv2V1ModelProperties.actionProfile = testObject->checkedTo<Bmv2V1ModelActionProfile>();
bmv2V1ModelProperties.actionSelector = testObject->checkedTo<Bmv2V1ModelActionSelector>();
bmv2V1ModelProperties.actionProfile = bmv2V1ModelProperties.actionSelector->getActionProfile();
bmv2V1ModelProperties.addProfileToState = false;
return true;
}
Expand Down Expand Up @@ -378,8 +379,7 @@ void Bmv2V1ModelTableStepper::checkTargetProperties(

// Check whether the table has an action selector associated with it.
if (checkForActionSelector()) {
// TODO: This should be a selector. Implement.
bmv2V1ModelProperties.implementaton = TableImplementation::profile;
bmv2V1ModelProperties.implementaton = TableImplementation::selector;
return;
}
}
Expand All @@ -388,9 +388,17 @@ void Bmv2V1ModelTableStepper::evalTargetTable(
const std::vector<const IR::ActionListElement *> &tableActionList) {
const auto *keys = table->getKey();
const auto &testgenOptions = TestgenOptions::get();
auto hasMatchKeys = false;
for (const auto &keyProperties : properties.resolvedKeys) {
if (keyProperties.matchType != BMv2Constants::MATCH_KIND_SELECTOR) {
hasMatchKeys = true;
break;
}
}

// If we have no keys, there is nothing to match.
if (keys == nullptr) {
// If we have no match keys, there is nothing to match.
// Selector keys influence action selection within a group and are not table match keys.
if (keys == nullptr || !hasMatchKeys) {
// Either override the default action or fall back to executing it.
auto testBackend = testgenOptions.testBackend;
if (testBackend == "STF" && !properties.defaultIsImmutable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,6 @@ p4tools_add_xfail_reason(
p4tools_add_xfail_reason(
"testgen-p4c-bmv2-ptf"
"Error encountered when cleaning up action profile group"
# bmv2 issue with one-shot indirect table programming
# Tracked as https://github.com/p4lang/p4c/issues/5547
action_profile-bmv2.p4
action_profile_max_group_size_annotation.p4
action_profile_sum_of_members_annotation.p4
# This was previously in this file under a different failure. May revert to
# "terminate called after throwing" when this one is fixed.
action_selector_shared-bmv2.p4
issue297-bmv2.p4
)

####################################################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@
#include <gtest/gtest.h>

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <map>
#include <sstream>
#include <vector>

#include "ir/ir.h"
#include "ir/irutils.h"
#include "lib/exceptions.h"

#include "backends/p4tools/modules/testgen/targets/bmv2/test_spec.h"

namespace P4::P4Tools::Test {

using namespace P4::literals;

using ::testing::HasSubstr;
using ::testing::Not;

TableConfig PTFTest::getForwardTableConfig() {
ActionArg port(new IR::Parameter("port", IR::Direction::None, IR::Type_Bits::get(9)),
Expand Down Expand Up @@ -239,4 +245,90 @@ TEST_F(PTFTest, Ptf04) {
}
}

TEST_F(PTFTest, PtfActionSelectorProgramming) {
const auto *payload =
IR::Constant::get(IR::Type_Bits::get(112), big_int("0x0000010100000202030355667788"));
const auto *payloadMask =
IR::Constant::get(IR::Type_Bits::get(112), big_int("0x0000000000000000000000000000"));
const auto ingressPacket = Packet(0, payload, payloadMask);
const auto egressPacket = Packet(2, payload, payloadMask);

auto selectorConfig = getForwardTableConfig();
const auto *apDecl = new IR::P4Table("selector_profile_decl", new IR::TableProperties());
const auto actionProfile = P4Testgen::Bmv2::Bmv2V1ModelActionProfile(apDecl);
const auto *asDecl = new IR::P4Table("selector_decl", new IR::TableProperties());
const auto actionSelector = P4Testgen::Bmv2::Bmv2V1ModelActionSelector(asDecl, &actionProfile);
selectorConfig.addTableProperty("action_profile"_cs, &actionProfile);
selectorConfig.addTableProperty("action_selector"_cs, &actionSelector);

auto testSpec = TestSpec(ingressPacket, egressPacket, {});
testSpec.addTestObject("tables"_cs, "SwitchIngress.forward"_cs, &selectorConfig);

const auto fileBasePath = std::filesystem::path("/tmp/p4c-bmv2-ptf-selector-test");
TestBackendConfiguration testBackendConfiguration{"selector_programming"_cs, 1, fileBasePath,
1};
auto testWriter = PTF(testBackendConfiguration);
testWriter.writeTestToFile(&testSpec, cstring::empty, 5, 0);

auto generatedFile = fileBasePath;
generatedFile.replace_extension(".py");
std::ifstream stream(generatedFile);
ASSERT_TRUE(stream.is_open());
std::stringstream buffer;
buffer << stream.rdbuf();
const auto rendered = buffer.str();
EXPECT_THAT(rendered, HasSubstr("self.send_request_add_member("));
EXPECT_THAT(rendered, HasSubstr("self.send_request_add_group("));
EXPECT_THAT(rendered, HasSubstr("self.send_request_add_entry_to_group("));
EXPECT_THAT(rendered, HasSubstr("'selector_profile_decl'"));
EXPECT_THAT(rendered, Not(HasSubstr("'selector_decl'")));
std::filesystem::remove(generatedFile);
}

TEST_F(PTFTest, PtfActionSelectorSharedProfileIdsAreUniqueAcrossTables) {
const auto *payload =
IR::Constant::get(IR::Type_Bits::get(112), big_int("0x0000010100000202030355667788"));
const auto *payloadMask =
IR::Constant::get(IR::Type_Bits::get(112), big_int("0x0000000000000000000000000000"));
const auto ingressPacket = Packet(0, payload, payloadMask);
const auto egressPacket = Packet(2, payload, payloadMask);

auto selectorConfig1 = getForwardTableConfig();
auto selectorConfig2 = getForwardTableConfig();
const auto *apDecl = new IR::P4Table("hashed_selector", new IR::TableProperties());
const auto actionProfile = P4Testgen::Bmv2::Bmv2V1ModelActionProfile(apDecl);
const auto *asDecl1 = new IR::P4Table("selector_decl_1", new IR::TableProperties());
const auto *asDecl2 = new IR::P4Table("selector_decl_2", new IR::TableProperties());
const auto actionSelector1 =
P4Testgen::Bmv2::Bmv2V1ModelActionSelector(asDecl1, &actionProfile);
const auto actionSelector2 =
P4Testgen::Bmv2::Bmv2V1ModelActionSelector(asDecl2, &actionProfile);
selectorConfig1.addTableProperty("action_profile"_cs, &actionProfile);
selectorConfig1.addTableProperty("action_selector"_cs, &actionSelector1);
selectorConfig2.addTableProperty("action_profile"_cs, &actionProfile);
selectorConfig2.addTableProperty("action_selector"_cs, &actionSelector2);

auto testSpec = TestSpec(ingressPacket, egressPacket, {});
testSpec.addTestObject("tables"_cs, "SwitchIngress.forward_1"_cs, &selectorConfig1);
testSpec.addTestObject("tables"_cs, "SwitchIngress.forward_2"_cs, &selectorConfig2);

const auto fileBasePath = std::filesystem::path("/tmp/p4c-bmv2-ptf-selector-shared-profile");
TestBackendConfiguration testBackendConfiguration{"selector_shared_profile"_cs, 1, fileBasePath,
1};
auto testWriter = PTF(testBackendConfiguration);
testWriter.writeTestToFile(&testSpec, cstring::empty, 7, 0);

auto generatedFile = fileBasePath;
generatedFile.replace_extension(".py");
std::ifstream stream(generatedFile);
ASSERT_TRUE(stream.is_open());
std::stringstream buffer;
buffer << stream.rdbuf();
const auto rendered = buffer.str();
EXPECT_THAT(rendered, HasSubstr("'hashed_selector'"));
EXPECT_THAT(rendered, HasSubstr("'hashed_selector',\n 1,"));
EXPECT_THAT(rendered, HasSubstr("'hashed_selector',\n 2,"));
std::filesystem::remove(generatedFile);
}

} // namespace P4::P4Tools::Test
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ inja::json Bmv2TestFramework::getControlPlane(const TestSpec *testSpec) const {
auto controlPlaneJson = inja::json::object();
// Map of actionProfiles and actionSelectors for easy reference.
std::map<cstring, cstring> apAsMap;
// Action profile member/group IDs are profile-scoped in P4Runtime.
std::map<std::string, size_t> profileRuleIds;

auto tables = testSpec->getTestObjectCategory("tables"_cs);
if (!tables.empty()) {
Expand All @@ -102,6 +104,19 @@ inja::json Bmv2TestFramework::getControlPlane(const TestSpec *testSpec) const {
checkForTableActionProfile<Bmv2V1ModelActionProfile, Bmv2V1ModelActionSelector>(
tblJson, apAsMap, tblConfig);

if (tblJson.contains("has_as")) {
const auto &profileName = tblJson["action_profile"].get_ref<const std::string &>();
auto &nextId = profileRuleIds[profileName];
if (nextId == 0) {
nextId = 1;
}
for (auto &rule : tblJson["rules"]) {
rule["profile_member_id"] = nextId;
rule["profile_group_id"] = nextId;
++nextId;
}
}

// Check whether the default action is overridden for this table.
checkForDefaultActionOverride(tblJson, tblConfig);

Expand Down
46 changes: 46 additions & 0 deletions backends/p4tools/modules/testgen/targets/bmv2/test_backend/ptf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,51 @@ class Test{{test_id}}(AbstractTest):
## if control_plane
## for table in control_plane.tables
## for rule in table.rules
## if existsIn(table, "has_as")
self.send_request_add_member(
'{{table.action_profile}}',
{{rule.profile_member_id}},
'{{rule.action_name}}',
[
## for act_param in rule.rules.act_args
('{{act_param.param}}', {{act_param.value}}),
## endfor
]
)
self.send_request_add_group(
'{{table.action_profile}}',
{{rule.profile_group_id}},
1,
[{{rule.profile_member_id}}]
)
## endif
## endfor
## endfor
## for table in control_plane.tables
## for rule in table.rules
## if existsIn(table, "has_as")
self.send_request_add_entry_to_group(
'{{table.table_name}}',
[
## for r in rule.rules.single_exact_matches
self.Exact('{{r.field_name}}', {{r.value}}),
## endfor
## for r in rule.rules.optional_matches
self.Optional('{{r.field_name}}', {{r.value}}, {{r.use_exact}}),
## endfor
## for r in rule.rules.range_matches
self.Range('{{r.field_name}}', {{r.lo}}, {{r.hi}}),
## endfor
## for r in rule.rules.ternary_matches
self.Ternary('{{r.field_name}}', {{r.value}}, {{r.mask}}),
## endfor
## for r in rule.rules.lpm_matches
self.Lpm('{{r.field_name}}', {{r.value}}, {{r.prefix_len}}),
## endfor
],
{{rule.profile_group_id}}
)
## else
self.table_add(
('{{table.table_name}}',
[
Expand Down Expand Up @@ -210,6 +255,7 @@ class Test{{test_id}}(AbstractTest):
, {% if rule.rules.needs_priority %}{{rule.priority}}{% else %}None{% endif %}
{% if existsIn(table, "has_ap") %}, {"oneshot": True}{% endif %}
)
## endif
## endfor
## endfor
## endif
Expand Down
4 changes: 3 additions & 1 deletion backends/p4tools/modules/testgen/targets/bmv2/test_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ Bmv2V1ModelActionSelector::Bmv2V1ModelActionSelector(const IR::IDeclaration *sel
const Bmv2V1ModelActionProfile *actionProfile)
: selectorDecl(selectorDecl), actionProfile(actionProfile) {}

cstring Bmv2V1ModelActionSelector::getObjectName() const { return "Bmv2V1ModelActionSelector"_cs; }
cstring Bmv2V1ModelActionSelector::getObjectName() const {
return selectorDecl->controlPlaneName();
}

const IR::IDeclaration *Bmv2V1ModelActionSelector::getSelectorDecl() const { return selectorDecl; }

Expand Down
Loading
Loading