diff --git a/tests/integration/system/BUILD.bazel b/tests/integration/system/BUILD.bazel index c880409f2..57cb0a6a3 100644 --- a/tests/integration/system/BUILD.bazel +++ b/tests/integration/system/BUILD.bazel @@ -67,3 +67,47 @@ cc_library( ], alwayslink = True, ) + +cc_library( + name = "packet_forwarding_tests", + testonly = True, + srcs = ["packet_forwarding_tests.cc"], + hdrs = ["packet_forwarding_tests.h"], + deps = [ + "//gutil:collections", + "//gutil:status_matchers", + "//gutil:testing", + "//lib/basic_traffic", + "//lib/basic_traffic:basic_p4rt_util", + "//lib/gnmi:gnmi_helper", + "//lib/p4rt:p4rt_programming_context", + "//lib/utils:generic_testbed_utils", + "//p4_pdpi:ir", + "//p4_pdpi:ir_cc_proto", + "//p4_pdpi:p4_runtime_session", + "//p4_pdpi/packetlib", + "//p4_pdpi/packetlib:packetlib_cc_proto", + "//sai_p4/instantiations/google:sai_pd_cc_proto", + "//tests/lib:switch_test_setup_helpers", + "//thinkit:control_device", + "//thinkit:generic_testbed", + "//thinkit:generic_testbed_fixture", + "//thinkit:switch", + "//thinkit/proto:generic_testbed_cc_proto", + "@com_github_gnmi//proto/gnmi:gnmi_cc_grpc_proto", + "@com_github_google_glog//:glog", + "@com_github_p4lang_p4runtime//:p4info_cc_proto", + "@com_github_p4lang_p4runtime//:p4runtime_cc_proto", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/numeric:int128", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@com_google_googletest//:gtest", + ], + alwayslink = True, +) diff --git a/tests/integration/system/packet_forwarding_tests.cc b/tests/integration/system/packet_forwarding_tests.cc new file mode 100644 index 000000000..06caae076 --- /dev/null +++ b/tests/integration/system/packet_forwarding_tests.cc @@ -0,0 +1,342 @@ +// Copyright (c) 2024, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "tests/integration/system/packet_forwarding_tests.h" + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/flags/flag.h" +#include "absl/numeric/int128.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "absl/synchronization/mutex.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "glog/logging.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gutil/collections.h" +#include "gutil/status.h" +#include "gutil/status_matchers.h" +#include "gutil/testing.h" +#include "lib/basic_traffic/basic_p4rt_util.h" +#include "lib/basic_traffic/basic_traffic.h" +#include "lib/gnmi/gnmi_helper.h" +#include "lib/p4rt/p4rt_programming_context.h" +#include "lib/utils/generic_testbed_utils.h" +#include "p4/config/v1/p4info.pb.h" +#include "p4/v1/p4runtime.pb.h" +#include "p4_pdpi/ir.h" +#include "p4_pdpi/ir.pb.h" +#include "p4_pdpi/p4_runtime_session.h" +#include "p4_pdpi/packetlib/packetlib.h" +#include "p4_pdpi/packetlib/packetlib.pb.h" +#include "proto/gnmi/gnmi.grpc.pb.h" +#include "sai_p4/instantiations/google/sai_pd.pb.h" +#include "tests/lib/switch_test_setup_helpers.h" +#include "thinkit/control_device.h" +#include "thinkit/generic_testbed.h" +#include "thinkit/proto/generic_testbed.pb.h" +#include "thinkit/switch.h" + +ABSL_FLAG(bool, push_p4_info, true, "Push P4 info to SUT."); + +namespace pins_test { +namespace { + +constexpr int kMtu[] = {1500, 5000, 9000}; + +// Test packet proto message sent from control switch to sut. +constexpr absl::string_view kTestPacket = R"pb( + headers { + ethernet_header { + ethernet_destination: "02:03:04:05:06:07" + ethernet_source: "00:01:02:03:04:05" + ethertype: "0x0800" + } + } + headers { + ipv4_header { + version: "0x4" + ihl: "0x5" + dscp: "0x03" + ecn: "0x0" + identification: "0x0000" + flags: "0x0" + fragment_offset: "0x0000" + ttl: "0x20" + protocol: "0x11" + ipv4_source: "1.2.3.4" + ipv4_destination: "$0" + } + } + headers { udp_header { source_port: "0x0000" destination_port: "0x0000" } } + payload: "Basic L3 test packet")pb"; + +// Pushes the P4 Info to SUT if the flag push_p4_info is set to true and returns +// the P4 Runtime Session +absl::StatusOr> P4InfoPush( + const p4::config::v1::P4Info& p4_info, thinkit::GenericTestbed& testbed) { + std::optional p4info = std::nullopt; + if (absl::GetFlag(FLAGS_push_p4_info)) { + p4info = p4_info; + } + return ConfigureSwitchAndReturnP4RuntimeSession( + testbed.Sut(), /*gnmi_config=*/std::nullopt, p4info); +} + +TEST_P(PacketForwardingTestFixture, PacketForwardingTest) { + thinkit::TestRequirements requirements = + gutil::ParseProtoOrDie( + R"pb(interface_requirements { + count: 2 + interface_mode: CONTROL_INTERFACE + })pb"); + + ASSERT_OK_AND_ASSIGN(auto testbed, GetTestbedWithRequirements(requirements)); + std::vector control_links = + FromTestbed(GetAllControlLinks, *testbed); + + ASSERT_OK_AND_ASSIGN(auto stub, testbed->Sut().CreateGnmiStub()); + ASSERT_OK_AND_ASSIGN(auto port_id_by_interface, + GetAllInterfaceNameToPortId(*stub)); + + // Set the `source_link` to the first SUT control link. + const InterfaceLink& source_link = control_links[0]; + ASSERT_OK_AND_ASSIGN( + std::string source_port_id_value, + gutil::FindOrStatus(port_id_by_interface, source_link.sut_interface)); + + // Set the `destination_link` to the second SUT control link. + const InterfaceLink& destination_link = control_links[1]; + ASSERT_OK_AND_ASSIGN(std::string destination_port_id_value, + gutil::FindOrStatus(port_id_by_interface, + destination_link.sut_interface)); + + LOG(INFO) << "Source port: " << source_link.sut_interface + << " port id: " << source_port_id_value; + LOG(INFO) << "Destination port: " << destination_link.sut_interface + << " port id: " << destination_port_id_value; + + ASSERT_OK_AND_ASSIGN(std::unique_ptr p4_session, + P4InfoPush(GetParam().p4_info, *testbed)); + // Set up a route between the source and destination interfaces. + ASSERT_OK_AND_ASSIGN(auto port_id_from_sut_interface, + GetAllInterfaceNameToPortId(*stub)); + ASSERT_OK_AND_ASSIGN(const pdpi::IrP4Info ir_p4info, + pdpi::CreateIrP4Info(GetParam().p4_info)); + P4rtProgrammingContext p4rt_context(p4_session.get(), + pdpi::SetMetadataAndSendPiWriteRequest); + ASSERT_OK(basic_traffic::ProgramRoutes( + p4rt_context.GetWriteRequestFunction(), ir_p4info, + port_id_from_sut_interface, + {{.ingress_interface = source_link.sut_interface, + .egress_interface = destination_link.sut_interface}})); + + int destination_port_id; + ASSERT_TRUE( + absl::SimpleAtoi(destination_port_id_value, &destination_port_id)); + + // Make test packet based on destination port ID. + const auto test_packet = + gutil::ParseProtoOrDie(absl::Substitute( + kTestPacket, basic_traffic::PortIdToIP(destination_port_id))); + ASSERT_OK_AND_ASSIGN(std::string test_packet_data, + packetlib::SerializePacket(test_packet)); + + absl::Mutex mutex; + std::vector received_packets; + static constexpr int kPacketsToSend = 10; + { + ASSERT_OK_AND_ASSIGN(auto finalizer, + testbed.get()->ControlDevice().CollectPackets()); + + LOG(INFO) << "Sending Packet to " << source_link.peer_interface; + LOG(INFO) << "Test packet data: " << test_packet.DebugString(); + + for (int i = 0; i < kPacketsToSend; i++) { + // Send packet to SUT. + ASSERT_OK(testbed->ControlDevice().SendPacket(source_link.peer_interface, + test_packet_data)) + << "failed to inject the packet."; + LOG(INFO) << "SendPacket completed"; + } + ASSERT_OK(finalizer->HandlePacketsFor( + absl::Seconds(30), + [&](absl::string_view interface, absl::string_view packet) { + if (interface != destination_link.peer_interface) { + return; + } + packetlib::Packet parsed_packet = packetlib::ParsePacket(packet); + if (!absl::StrContains(parsed_packet.payload(), + "Basic L3 test packet")) { + return; + } + absl::MutexLock lock(&mutex); + received_packets.push_back(std::string(packet)); + })); + } + EXPECT_EQ(received_packets.size(), kPacketsToSend); +} + +using InterfaceCounters = absl::flat_hash_map; + +TEST_P(PacketForwardingTestFixture, AllPortsPacketForwardingTest) { + thinkit::TestRequirements requirements = + gutil::ParseProtoOrDie( + R"pb(interface_requirements { + count: 2 + interface_mode: CONTROL_INTERFACE + })pb"); + + ASSERT_OK_AND_ASSIGN(auto testbed, GetTestbedWithRequirements(requirements)); + ASSERT_OK_AND_ASSIGN(auto gnmi_stub, testbed->Sut().CreateGnmiStub()); + + std::vector sut_interfaces = + GetSutInterfaces(FromTestbed(GetAllControlLinks, *testbed)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr p4_session, + P4InfoPush(GetParam().p4_info, *testbed)); + ASSERT_OK_AND_ASSIGN(const pdpi::IrP4Info ir_p4info, + pdpi::CreateIrP4Info(GetParam().p4_info)); + + // Collect counters before the test. + ASSERT_OK_AND_ASSIGN(InterfaceCounters pretraffic_counters, + GetAllInterfaceCounters(*gnmi_stub)); + + const auto test_packet = + gutil::ParseProtoOrDie(kTestPacket); + ASSERT_OK_AND_ASSIGN( + auto statistics, + basic_traffic::SendTraffic(*testbed, p4_session.get(), ir_p4info, + basic_traffic::AllToAll(sut_interfaces), + {test_packet}, absl::Minutes(5))); + + // Collate traffic statistics to dump to artifacts. + std::string packet_statistics_csv = + "ingress_interface,egress_interface,packets_sent,packets_received\n"; + for (const basic_traffic::TrafficStatistic& statistic : statistics) { + absl::StrAppend(&packet_statistics_csv, + absl::StrJoin({statistic.interfaces.ingress_interface, + statistic.interfaces.egress_interface, + absl::StrCat(statistic.packets_sent), + absl::StrCat(statistic.packets_received)}, + ","), + "\n"); + EXPECT_EQ(statistic.packets_sent, statistic.packets_received); + EXPECT_EQ(statistic.packets_routed_incorrectly, 0); + } + EXPECT_OK(testbed->Environment().StoreTestArtifact("traffic_statistics.csv", + packet_statistics_csv)); + + // Calculate counter differences and dump to artifacts. + std::string sut_counters_csv = "interface,in_packets,out_packets\n"; + ASSERT_OK_AND_ASSIGN(InterfaceCounters posttraffic_counters, + GetAllInterfaceCounters(*gnmi_stub)); + for (const std::string& interface : sut_interfaces) { + const Counters& pre = pretraffic_counters[interface]; + const Counters& post = posttraffic_counters[interface]; + absl::StrAppend( + &sut_counters_csv, + absl::StrJoin({interface, absl::StrCat(post.in_pkts - pre.in_pkts), + absl::StrCat(post.out_pkts - pre.out_pkts)}, + ","), + "\n"); + } + EXPECT_OK(testbed->Environment().StoreTestArtifact("sut_counters.csv", + sut_counters_csv)); +} + +TEST_P(PacketForwardingTestFixture, MtuPacketForwardingTest) { + thinkit::TestRequirements requirements = + gutil::ParseProtoOrDie( + R"pb(interface_requirements { + count: 2 + interface_mode: CONTROL_INTERFACE + })pb"); + + ASSERT_OK_AND_ASSIGN(auto testbed, GetTestbedWithRequirements(requirements)); + ASSERT_OK_AND_ASSIGN(auto gnmi_stub, testbed->Sut().CreateGnmiStub()); + std::vector sut_interfaces = + GetSutInterfaces(FromTestbed(GetAllControlLinks, *testbed)); + + ASSERT_OK_AND_ASSIGN(std::unique_ptr p4_session, + P4InfoPush(GetParam().p4_info, *testbed)); + ASSERT_OK_AND_ASSIGN(const pdpi::IrP4Info ir_p4info, + pdpi::CreateIrP4Info(GetParam().p4_info)); + + for (int mtu : kMtu) { + // Collect counters before the test. + ASSERT_OK_AND_ASSIGN(InterfaceCounters pretraffic_counters, + GetAllInterfaceCounters(*gnmi_stub)); + + LOG(INFO) << "MTU: " << mtu; + auto test_packet = gutil::ParseProtoOrDie(kTestPacket); + ASSERT_OK(PadPacket(mtu, test_packet)); + LOG(INFO) << "Packet: " << test_packet.DebugString(); + ASSERT_OK_AND_ASSIGN( + auto statistics, + basic_traffic::SendTraffic(*testbed, p4_session.get(), ir_p4info, + basic_traffic::AllToAll(sut_interfaces), + {test_packet}, absl::Minutes(5))); + + // Collate traffic statistics to dump to artifacts. + std::string packet_statistics_csv = + "ingress_interface,egress_interface,packets_sent,packets_received\n"; + for (const basic_traffic::TrafficStatistic& statistic : statistics) { + absl::StrAppend(&packet_statistics_csv, + absl::StrJoin({statistic.interfaces.ingress_interface, + statistic.interfaces.egress_interface, + absl::StrCat(statistic.packets_sent), + absl::StrCat(statistic.packets_received)}, + ","), + "\n"); + EXPECT_EQ(statistic.packets_sent, statistic.packets_received); + EXPECT_EQ(statistic.packets_routed_incorrectly, 0); + } + EXPECT_OK(testbed->Environment().StoreTestArtifact( + absl::StrCat("traffic_statistics_mtu", mtu, ".csv"), + packet_statistics_csv)); + + // Calculate counter differences and dump to artifacts. + std::string sut_counters_csv = "interface,in_packets,out_packets\n"; + ASSERT_OK_AND_ASSIGN(InterfaceCounters posttraffic_counters, + GetAllInterfaceCounters(*gnmi_stub)); + for (const std::string& interface : sut_interfaces) { + const Counters& pre = pretraffic_counters[interface]; + const Counters& post = posttraffic_counters[interface]; + absl::StrAppend( + &sut_counters_csv, + absl::StrJoin({interface, absl::StrCat(post.in_pkts - pre.in_pkts), + absl::StrCat(post.out_pkts - pre.out_pkts)}, + ","), + "\n"); + } + EXPECT_OK(testbed->Environment().StoreTestArtifact( + absl::StrCat("sut_counters_mtu", mtu, ".csv"), sut_counters_csv)); + } +} + +} // namespace +} // namespace pins_test diff --git a/tests/integration/system/packet_forwarding_tests.h b/tests/integration/system/packet_forwarding_tests.h new file mode 100644 index 000000000..86022d87b --- /dev/null +++ b/tests/integration/system/packet_forwarding_tests.h @@ -0,0 +1,26 @@ +// Copyright (c) 2024, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PINS_TESTS_INTEGRATION_SYSTEM_PACKET_FORWARDING_TESTS_H_ +#define PINS_TESTS_INTEGRATION_SYSTEM_PACKET_FORWARDING_TESTS_H_ + +#include "thinkit/generic_testbed_fixture.h" + +namespace pins_test { + +class PacketForwardingTestFixture : public thinkit::GenericTestbedFixture<> {}; + +} // namespace pins_test + +#endif // PINS_TESTS_INTEGRATION_SYSTEM_PACKET_FORWARDING_TESTS_H_ diff --git a/tests/lib/BUILD.bazel b/tests/lib/BUILD.bazel index 1e78dc02f..9d9307311 100644 --- a/tests/lib/BUILD.bazel +++ b/tests/lib/BUILD.bazel @@ -58,18 +58,57 @@ cc_library( srcs = ["switch_test_setup_helpers.cc"], hdrs = ["switch_test_setup_helpers.h"], deps = [ + "//gutil:collections", + "//gutil:proto", "//lib/gnmi:gnmi_helper", "//lib/gnmi:openconfig_cc_proto", "//lib/validator:validator_lib", + "//p4_pdpi:ir_cc_proto", + "//p4_pdpi:ir_tools", "//p4_pdpi:p4_runtime_session", + "//tests:thinkit_sanity_tests", "//thinkit:mirror_testbed", "//thinkit:switch", + "@com_github_google_glog//:glog", "@com_github_p4lang_p4runtime//:p4info_cc_proto", "@com_github_p4lang_p4runtime//:p4runtime_cc_proto", + "@com_github_p4lang_p4runtime//:p4types_cc_proto", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", "@com_google_absl//absl/types:optional", + "@com_google_absl//absl/types:span", + ], +) + +# go/golden-test-with-coverage +cc_test( + name = "switch_test_setup_helpers_golden_test_runner", + srcs = ["switch_test_setup_helpers_golden_test_runner.cc"], + data = ["switch_test_setup_helpers.expected"], + linkstatic = True, + deps = [ + ":switch_test_setup_helpers", + "//gutil:status_matchers", + "//gutil:testing", + "//p4_pdpi:ir_cc_proto", + "//sai_p4/instantiations/google:instantiations", + "//sai_p4/instantiations/google:sai_p4info_cc", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf", + ], +) + +cmd_diff_test( + name = "switch_test_setup_helpers_golden_test", + actual_cmd = "$(execpath :switch_test_setup_helpers_golden_test_runner)", + expected = "//tests/lib:switch_test_setup_helpers.expected", + tools = [ + ":switch_test_setup_helpers_golden_test_runner", ], ) diff --git a/tests/lib/switch_test_setup_helpers.cc b/tests/lib/switch_test_setup_helpers.cc index bca2c5688..57bf789e7 100644 --- a/tests/lib/switch_test_setup_helpers.cc +++ b/tests/lib/switch_test_setup_helpers.cc @@ -1,35 +1,49 @@ #include "tests/lib/switch_test_setup_helpers.h" +#include +#include +#include // NOLINT: third_party library. #include #include #include #include +#include +#include "absl/container/btree_set.h" +#include "absl/container/flat_hash_map.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/optional.h" +#include "absl/types/span.h" +#include "glog/logging.h" +#include "gutil/collections.h" +#include "gutil/proto.h" #include "gutil/status.h" #include "lib/gnmi/gnmi_helper.h" #include "lib/gnmi/openconfig.pb.h" #include "lib/validator/validator_lib.h" +#include "p4/config/v1/p4types.pb.h" #include "p4/v1/p4runtime.pb.h" +#include "p4_pdpi/ir_tools.h" #include "p4_pdpi/p4_runtime_session.h" +#include "tests/thinkit_sanity_tests.h" namespace pins_test { namespace { constexpr absl::Duration kGnmiTimeoutDefault = absl::Minutes(3); +constexpr char kPortNamedType[] = "port_id_t"; -absl::StatusOr> -CreateP4RuntimeSessionAndClearExistingState( +absl::Status ClearTableEntries( thinkit::Switch& thinkit_switch, const pdpi::P4RuntimeSessionOptionalArgs& metadata) { ASSIGN_OR_RETURN(std::unique_ptr session, pdpi::P4RuntimeSession::Create(thinkit_switch, metadata)); RETURN_IF_ERROR(pdpi::ClearTableEntries(session.get())); - RETURN_IF_ERROR(pdpi::CheckNoTableEntries(session.get())).SetPrepend() - << "cleared all table entries: "; - return session; + RETURN_IF_ERROR(session->Finish()); + return absl::OkStatus(); } absl::Status PushGnmiAndWaitForConvergence(thinkit::Switch& thinkit_switch, @@ -40,13 +54,69 @@ absl::Status PushGnmiAndWaitForConvergence(thinkit::Switch& thinkit_switch, gnmi_timeout); } -absl::Status SetForwardingPipelineConfigAndAssureClearedTables( - pdpi::P4RuntimeSession* session, const p4::config::v1::P4Info& p4info) { - RETURN_IF_ERROR(pdpi::SetMetadataAndSetForwardingPipelineConfig( - session, p4::v1::SetForwardingPipelineConfigRequest::RECONCILE_AND_COMMIT, - p4info)); - RETURN_IF_ERROR(pdpi::CheckNoTableEntries(session)).SetPrepend() - << "set a new forwarding pipeline config: "; +absl::StatusOr> +CreateP4RuntimeSessionAndOptionallyPushP4Info( + thinkit::Switch& thinkit_switch, + std::optional p4info, + const pdpi::P4RuntimeSessionOptionalArgs& metadata) { + ASSIGN_OR_RETURN(std::unique_ptr session, + pdpi::P4RuntimeSession::Create(thinkit_switch, metadata)); + + if (p4info.has_value()) { + // Check if P4Info already exists, and if so reboot to workaround PINS + // limitations (b/200209778). + ASSIGN_OR_RETURN(p4::v1::GetForwardingPipelineConfigResponse response, + pdpi::GetForwardingPipelineConfig(session.get())); + ASSIGN_OR_RETURN(std::string p4info_diff, + gutil::ProtoDiff(*p4info, response.config().p4info())); + if (response.config().has_p4info() && !p4info_diff.empty()) { + LOG(WARNING) + << "Rebooting since P4Info reconfiguration is unsupported by PINS, " + "but I am asked to push a P4Info with the following diff:\n" + << p4info_diff; + RETURN_IF_ERROR(session->Finish()); + TestGnoiSystemColdReboot(thinkit_switch); + // Reconnect after reboot. + ASSIGN_OR_RETURN( + session, pdpi::P4RuntimeSession::Create(thinkit_switch, metadata)); + } + + // Push P4Info. + RETURN_IF_ERROR(pdpi::SetMetadataAndSetForwardingPipelineConfig( + session.get(), + p4::v1::SetForwardingPipelineConfigRequest::RECONCILE_AND_COMMIT, + *p4info)); + } + + RETURN_IF_ERROR(pdpi::CheckNoTableEntries(session.get())); + return session; +} + +// Uses the `port_map` to remap any P4runtime ports in `entries`. +absl::Status RewritePortsInTableEntries( + const pdpi::IrP4Info& info, std::vector& entries, + const absl::flat_hash_map& port_map) { + p4::config::v1::P4NamedType port_type; + port_type.set_name(kPortNamedType); + RETURN_IF_ERROR(pdpi::TransformValuesOfType( + info, port_type, entries, [&](absl::string_view old_port) { + return gutil::FindOrStatus(port_map, std::string(old_port)); + })); + + // Watch ports do not have a named type, but we still consider them ports so + // we have to deal with them specifically rather than using the generic + // rewriting function above. + for (pdpi::IrTableEntry& entry : entries) { + if (entry.has_action_set()) { + for (auto& action : *entry.mutable_action_set()->mutable_actions()) { + if (!action.watch_port().empty()) { + ASSIGN_OR_RETURN(*action.mutable_watch_port(), + gutil::FindOrStatus(port_map, action.watch_port())); + } + } + } + } + return absl::OkStatus(); } @@ -59,9 +129,7 @@ ConfigureSwitchAndReturnP4RuntimeSession( const pdpi::P4RuntimeSessionOptionalArgs& metadata) { // Since the gNMI Config push relies on tables being cleared, we construct a // P4RuntimeSession and clear the tables first. - ASSIGN_OR_RETURN( - std::unique_ptr session, - CreateP4RuntimeSessionAndClearExistingState(thinkit_switch, metadata)); + RETURN_IF_ERROR(ClearTableEntries(thinkit_switch, metadata)); if (gnmi_config.has_value()) { RETURN_IF_ERROR( @@ -69,12 +137,8 @@ ConfigureSwitchAndReturnP4RuntimeSession( /*gnmi_timeout=*/kGnmiTimeoutDefault)); } - if (p4info.has_value()) { - RETURN_IF_ERROR(SetForwardingPipelineConfigAndAssureClearedTables( - session.get(), *p4info)); - } - - return session; + return CreateP4RuntimeSessionAndOptionallyPushP4Info(thinkit_switch, p4info, + metadata); } absl::StatusOr, @@ -84,33 +148,29 @@ ConfigureSwitchPairAndReturnP4RuntimeSessionPair( std::optional gnmi_config, std::optional p4info, const pdpi::P4RuntimeSessionOptionalArgs& metadata) { - // Since the gNMI Config push relies on tables being cleared, we construct the - // P4RuntimeSessions and clear the tables first. - ASSIGN_OR_RETURN( - std::unique_ptr session1, - CreateP4RuntimeSessionAndClearExistingState(thinkit_switch1, metadata)); - ASSIGN_OR_RETURN( - std::unique_ptr session2, - CreateP4RuntimeSessionAndClearExistingState(thinkit_switch2, metadata)); - - if (gnmi_config.has_value()) { - // Push the gNMI configs to both switches before waiting for convergence. - RETURN_IF_ERROR(PushGnmiConfig(thinkit_switch1, *gnmi_config)); - RETURN_IF_ERROR(PushGnmiConfig(thinkit_switch2, *gnmi_config)); - RETURN_IF_ERROR(WaitForGnmiPortIdConvergence(thinkit_switch1, *gnmi_config, - kGnmiTimeoutDefault)); - RETURN_IF_ERROR(WaitForGnmiPortIdConvergence(thinkit_switch2, *gnmi_config, - kGnmiTimeoutDefault)); - } - - if (p4info.has_value()) { - RETURN_IF_ERROR(SetForwardingPipelineConfigAndAssureClearedTables( - session1.get(), *p4info)); - RETURN_IF_ERROR(SetForwardingPipelineConfigAndAssureClearedTables( - session2.get(), *p4info)); + // We configure both switches in parallel, since it may require rebooting the + // switch which is costly. + using T = absl::StatusOr>; + T session1, session2; + { + std::future future1 = std::async(std::launch::async, [&] { + return ConfigureSwitchAndReturnP4RuntimeSession( + thinkit_switch1, gnmi_config, p4info, metadata); + }); + std::future future2 = std::async(std::launch::async, [&] { + return ConfigureSwitchAndReturnP4RuntimeSession( + thinkit_switch2, gnmi_config, p4info, metadata); + }); + session1 = future1.get(); + session2 = future2.get(); } - - return std::make_pair(std::move(session1), std::move(session2)); + RETURN_IF_ERROR(session1.status()).SetPrepend() + << "failed to configure switch '" << thinkit_switch1.ChassisName() + << "': "; + RETURN_IF_ERROR(session2.status()).SetPrepend() + << "failed to configure switch '" << thinkit_switch2.ChassisName() + << "': "; + return std::make_pair(std::move(*session1), std::move(*session2)); } absl::Status MirrorSutP4rtPortIdConfigToControlSwitch( @@ -181,4 +241,90 @@ absl::Status WaitForEnabledInterfacesToBeUp( /*with_healthz=*/false); } +// Gets every P4runtime port used in `entries`. +absl::StatusOr> GetPortsUsed( + const pdpi::IrP4Info& info, std::vector entries) { + absl::btree_set ports; + p4::config::v1::P4NamedType port_type; + port_type.set_name(kPortNamedType); + RETURN_IF_ERROR(pdpi::TransformValuesOfType(info, port_type, entries, + [&](absl::string_view port) { + ports.insert(std::string(port)); + return std::string(port); + })); + + // Watch ports do not have a named type, but we still consider them ports so + // we have to deal with them specifically rather than using the generic + // function above. + for (pdpi::IrTableEntry& entry : entries) { + if (entry.has_action_set()) { + for (const auto& action : entry.action_set().actions()) { + if (!action.watch_port().empty()) { + ports.insert(action.watch_port()); + } + } + } + } + + return ports; +} + +// Remaps ports in a round-robin fashion, but starts by fixing those that are +// both used in `entries` and in `ports`. +absl::Status RewritePortsInTableEntries( + const pdpi::IrP4Info& info, absl::Span new_ports, + std::vector& entries) { + if (new_ports.empty()) { + return absl::InvalidArgumentError("`new_ports` may not be empty"); + } + + // We get the ports used in the original entries so we can preserve any that + // exist in both, and then remap them in a round-robin fashion. + ASSIGN_OR_RETURN(absl::btree_set ports_used_originally, + GetPortsUsed(info, entries)); + + absl::flat_hash_map old_to_new_port_id; + // Queue of which new port to map an old port to next. + std::deque new_port_queue; + + for (const auto& new_port : new_ports) { + if (ports_used_originally.contains(new_port)) { + // Make sure that existing ports are preserved and add them to the back of + // the queue for balancing. + old_to_new_port_id[new_port] = new_port; + new_port_queue.push_back(new_port); + } else { + new_port_queue.push_front(new_port); + } + } + + // Map all old ports to new ports. + for (const auto& old_port : ports_used_originally) { + // If a port is already mapped, we should not remap it. + if (old_to_new_port_id.contains(old_port)) continue; + const std::string new_port = new_port_queue.front(); + old_to_new_port_id[old_port] = new_port; + new_port_queue.pop_front(); + new_port_queue.push_back(new_port); + } + + return RewritePortsInTableEntries(info, entries, old_to_new_port_id); +} + +absl::Status RewritePortsInTableEntries( + const pdpi::IrP4Info& info, absl::string_view gnmi_config, + std::vector& entries) { + ASSIGN_OR_RETURN(absl::btree_set valid_port_ids_set, + GetAllPortIds(gnmi_config)); + + if (valid_port_ids_set.empty()) { + return absl::InvalidArgumentError( + "given gnmi_config had no valid port ids"); + } + + std::vector new_ports(valid_port_ids_set.begin(), + valid_port_ids_set.end()); + return RewritePortsInTableEntries(info, new_ports, entries); +} + } // namespace pins_test diff --git a/tests/lib/switch_test_setup_helpers.expected b/tests/lib/switch_test_setup_helpers.expected index 47b1aeca7..75753c17e 100644 --- a/tests/lib/switch_test_setup_helpers.expected +++ b/tests/lib/switch_test_setup_helpers.expected @@ -246,7 +246,7 @@ matches { } } action { - name: "set_nexthop" + name: "set_ip_nexthop" params { name: "router_interface_id" value { @@ -726,4 +726,3 @@ modified: action_set.actions[3].watch_port: "14" -> "1" modified: action_set.actions[4].watch_port: "15" -> "a_port" modified: action_set.actions[5].watch_port: "16" -> "2" modified: action_set.actions[6].watch_port: "17" -> "1" - diff --git a/tests/lib/switch_test_setup_helpers.h b/tests/lib/switch_test_setup_helpers.h index a54942025..ee29d65bd 100644 --- a/tests/lib/switch_test_setup_helpers.h +++ b/tests/lib/switch_test_setup_helpers.h @@ -5,12 +5,16 @@ #include #include #include +#include +#include "absl/container/btree_set.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "absl/time/time.h" #include "lib/gnmi/openconfig.pb.h" #include "p4/config/v1/p4info.pb.h" +#include "p4_pdpi/ir.pb.h" #include "p4_pdpi/p4_runtime_session.h" #include "thinkit/mirror_testbed.h" #include "thinkit/switch.h" @@ -64,6 +68,32 @@ absl::Status WaitForEnabledInterfacesToBeUp( std::function> on_failure = std::nullopt); +// Gets a count of which P4runtime ports are used in `entries` and how +// frequently. +absl::StatusOr> GetPortsUsed( + const pdpi::IrP4Info& info, std::vector entries); + +// Rewrites the ports of the given table `entries` to only use the given +// non-empty `ports`. Uses `info` to determine which values refer to ports. +// Specifically, for each port `x` in the set of table entries, this +// function remaps it to f(x) such that: +// - if `x \in ports` then `f(x) = x` +// - `x` is remapped to an `f(x) \in ports` (chosen deterministically) +// such that `\forall. p1,p2 \in ports. |{f(x) = p1 | x \in +// all_ports(entries)}| <= |{f(x) = p2 | x \in all_ports(entries)}| + 1`. +// +// This function makes it possible to use a list of pre-existing table entries +// with any set of ports desired (or configured on the switch). +absl::Status RewritePortsInTableEntries( + const pdpi::IrP4Info& info, absl::Span new_ports, + std::vector& entries); + +// Extracts the available ports from the given `gnmi_config` and rewrites +// entries as per above. +absl::Status RewritePortsInTableEntries( + const pdpi::IrP4Info& info, absl::string_view gnmi_config, + std::vector& entries); + } // namespace pins_test #endif // PINS_TESTS_LIB_SWITCH_TEST_SETUP_HELPERS_H_ diff --git a/tests/lib/switch_test_setup_helpers_golden_test_runner.cc b/tests/lib/switch_test_setup_helpers_golden_test_runner.cc index 2c32e80d3..b12bf806e 100644 --- a/tests/lib/switch_test_setup_helpers_golden_test_runner.cc +++ b/tests/lib/switch_test_setup_helpers_golden_test_runner.cc @@ -307,7 +307,7 @@ int main(int argc, char** argv) { exact { str: "nexthop-1" } } action { - name: "set_nexthop" + name: "set_ip_nexthop" params { name: "router_interface_id" value { str: "router-interface-1" } diff --git a/tests/lib/switch_test_setup_helpers_test.cc b/tests/lib/switch_test_setup_helpers_test.cc index 4f4812ab3..23182badf 100644 --- a/tests/lib/switch_test_setup_helpers_test.cc +++ b/tests/lib/switch_test_setup_helpers_test.cc @@ -1,22 +1,38 @@ #include "tests/lib/switch_test_setup_helpers.h" +#include +#include #include +#include #include +#include +#include "absl/memory/memory.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "absl/strings/substitute.h" #include "absl/time/time.h" +#include "absl/types/span.h" #include "gmock/gmock.h" #include "grpcpp/test/mock_stream.h" #include "gtest/gtest.h" +#include "gutil/status.h" +#include "gutil/testing.h" #include "lib/gnmi/gnmi_helper.h" #include "p4/config/v1/p4info.pb.h" #include "p4/v1/p4runtime.pb.h" #include "p4/v1/p4runtime_mock.grpc.pb.h" +#include "p4_pdpi/ir.pb.h" #include "p4_pdpi/p4_runtime_session.h" +#include "p4_pdpi/pd.h" #include "p4_pdpi/testing/test_p4info.h" #include "proto/gnmi/gnmi_mock.grpc.pb.h" +#include "sai_p4/instantiations/google/instantiations.h" +#include "sai_p4/instantiations/google/sai_p4info.h" +#include "sai_p4/instantiations/google/sai_pd.pb.h" #include "thinkit/mock_switch.h" namespace pins_test { @@ -24,11 +40,14 @@ namespace { using ::testing::_; using ::testing::AnyNumber; -using ::testing::ByMove; using ::testing::EqualsProto; using ::testing::InSequence; +using ::testing::NiceMock; +using ::testing::Not; using ::testing::Return; using ::testing::ReturnRef; +using ::testing::StrictMock; +using ::testing::status::IsOk; // One of the tables and actions from // http://google3/blaze-out/genfiles/third_party/pins_infra/p4_pdpi/testing/test_p4info_embed.cc?l=13 @@ -96,8 +115,8 @@ void MockP4RuntimeSessionCreate( // be wrapped in is freed. The stream is wrapped in StreamChannel, which is // the method of the stub that calls StreamChannelRaw, but is not itself // mocked. - auto* stream = new grpc::testing::MockClientReaderWriter< - p4::v1::StreamMessageRequest, p4::v1::StreamMessageResponse>(); + auto* stream = new NiceMock>(); EXPECT_CALL(stub, StreamChannelRaw).WillOnce(Return(stream)); // A valid MasterArbitrationUpdate sent as request and response. @@ -146,14 +165,13 @@ p4::v1::WriteRequest ConstructDeleteRequest( // Mocks a `CheckNoEntries` call using the stub in a previously // mocked P4RuntimeSession. // Ensures that there are no table entries remaining. -void MockCheckNoEntries(p4::v1::MockP4RuntimeStub& stub, - const p4::config::v1::P4Info& p4info) { - // We need to return a valid p4info to get to the stage where we read tables. +void MockCheckNoEntries(p4::v1::MockP4RuntimeStub& stub) { + // We need to return a p4info to get to the stage where we read tables. EXPECT_CALL(stub, GetForwardingPipelineConfig) .WillOnce([=](auto, auto, p4::v1::GetForwardingPipelineConfigResponse* get_pipeline_response) { - *get_pipeline_response->mutable_config()->mutable_p4info() = p4info; + get_pipeline_response->mutable_config()->mutable_p4info(); return grpc::Status::OK; }); @@ -192,21 +210,44 @@ void MockClearTableEntries(p4::v1::MockP4RuntimeStub& stub, // Mocks a `CheckNoEntries` call, ensuring that the tables are really // cleared. - MockCheckNoEntries(stub, p4info); + MockCheckNoEntries(stub); } } +// Sets up expectation that `ClearTableEntries(mock_switch, metadata)` is +// called. +void ExpectCallToClearTableEntries( + thinkit::MockSwitch& mock_switch, const p4::config::v1::P4Info& p4info, + const pdpi::P4RuntimeSessionOptionalArgs& metadata) { + EXPECT_CALL(mock_switch, CreateP4RuntimeStub()).WillOnce([=] { + InSequence s; + auto stub = std::make_unique<::p4::v1::MockP4RuntimeStub>(); + MockP4RuntimeSessionCreate(*stub, metadata); + MockClearTableEntries(*stub, p4info, metadata); + return stub; + }); +} + // Mocks a successful `PushGnmiConfig` call. void MockGnmiPush(gnmi::MockgNMIStub& mock_gnmi_stub) { EXPECT_CALL(mock_gnmi_stub, Set).WillOnce(Return(grpc::Status::OK)); } +// Sets up expectation that `pins_test::PushGnmiConfig(mock_switch, _)` is +// called. +void ExpectCallToPushGnmiConfig(thinkit::MockSwitch& mock_switch) { + EXPECT_CALL(mock_switch, CreateGnmiStub).WillOnce([] { + auto mock_gnmi_stub = absl::make_unique(); + MockGnmiPush(*mock_gnmi_stub); + return mock_gnmi_stub; + }); +} + // Mocks a successful `WaitForGnmiPortIdConvergence` call where the ports given // by `interfaces` have converged. void MockGnmiConvergence( gnmi::MockgNMIStub& mock_gnmi_stub, - const std::vector& interfaces, - const absl::Duration& gnmi_timeout) { + const std::vector& interfaces) { EXPECT_CALL(mock_gnmi_stub, Get) .WillOnce([=](auto, auto, gnmi::GetResponse* response) { *response->add_notification() @@ -218,6 +259,30 @@ void MockGnmiConvergence( }); } +// Sets up expectation that +// `pins_test::WaitForGnmiPortIdConvergence(mock_switch, _, _)` is +// called, mocking response that the given`interfaces` have converged. +void ExpectCallToWaitForGnmiPortIdConvergence( + thinkit::MockSwitch& mock_switch, + const std::vector& interfaces) { + EXPECT_CALL(mock_switch, CreateGnmiStub).WillOnce([=] { + auto mock_gnmi_stub = absl::make_unique(); + MockGnmiConvergence(*mock_gnmi_stub, interfaces); + return mock_gnmi_stub; + }); +} + +// Sets up expectation that +// `pins_test::ExpectCallToPushGnmiAndWaitForConvergence(mock_switch, _, _)` is +// called, mocking response that the given`interfaces` have converged. +void ExpectCallToPushGnmiAndWaitForConvergence( + thinkit::MockSwitch& mock_switch, + const std::vector& interfaces) { + InSequence s; + ExpectCallToPushGnmiConfig(mock_switch); + ExpectCallToWaitForGnmiPortIdConvergence(mock_switch, interfaces); +} + // Constructs a valid forwarding pipeline config request with the given p4info // and metadata. p4::v1::SetForwardingPipelineConfigRequest @@ -237,6 +302,26 @@ ConstructForwardingPipelineConfigRequest( return request; } +void ExpectCallToCreateP4RuntimeSessionAndOptionallyPushP4Info( + thinkit::MockSwitch& mock_switch, + const std::optional& p4info, + const pdpi::P4RuntimeSessionOptionalArgs& metadata) { + EXPECT_CALL(mock_switch, CreateP4RuntimeStub).WillOnce([=] { + InSequence s; + auto stub = absl::make_unique>(); + MockP4RuntimeSessionCreate(*stub, metadata); + if (p4info.has_value()) { + // TODO: Test the path where `GetForwardingPipelineConfig` + // returns a non-empty P4Info. + EXPECT_CALL(*stub, GetForwardingPipelineConfig).Times(1); + EXPECT_CALL(*stub, SetForwardingPipelineConfig).Times(1); + } + MockCheckNoEntries(*stub); + + return stub; + }); +} + // Tests that ConfigureSwitchAndReturnP4RuntimeSession creates a // P4RuntimeSession, clears all table entries currently on the switch (mocked // to be one), pushes a gNMI config and converges (if config is given), @@ -250,16 +335,6 @@ void MockConfigureSwitchAndReturnP4RuntimeSession( const std::optional& p4info, const pdpi::P4RuntimeSessionOptionalArgs& metadata, const std::vector& interfaces) { - // The stub that will be returned when CreateP4RuntimeStub is called on - // mock_switch. - auto stub = absl::make_unique(); - ASSERT_NE(stub, nullptr); - - // The stubs that will be returned when CreateGnmiStub is called on - // mock_switch. - auto mock_gnmi_stub_push = absl::make_unique(); - auto mock_gnmi_stub_converge = absl::make_unique(); - // DeviceId and ChassisName may get called multiple times. The only important // point is that they always return the same response. ON_CALL(mock_switch, DeviceId).WillByDefault(Return(kDeviceId)); @@ -272,61 +347,15 @@ void MockConfigureSwitchAndReturnP4RuntimeSession( .WillByDefault(ReturnRef(*kChassisNameString)); EXPECT_CALL(mock_switch, ChassisName).Times(AnyNumber()); + // Actual test. { InSequence s; - // Mocks a P4RuntimeSession `Create` call. - // Constructs a ReaderWriter mock stream and completes an arbitration - // handshake. - MockP4RuntimeSessionCreate(*stub, metadata); - - // Mocks a `ClearTableEntries` call. - // Pulls the p4info from the switch, then reads a table entry, deletes it, - // and reads again ensuring that there are no table entries remaining. - MockClearTableEntries(*stub, pdpi::GetTestP4Info(), metadata); - - // Mocks a `CheckNoEntries` call. - MockCheckNoEntries(*stub, pdpi::GetTestP4Info()); - + ExpectCallToClearTableEntries(mock_switch, pdpi::GetTestP4Info(), metadata); if (gnmi_config.has_value()) { - // Mocks a `PushGnmiConfig` call. - MockGnmiPush(*mock_gnmi_stub_push); - // Mocks a `WaitForGnmiPortIdConvergence` call. - MockGnmiConvergence(*mock_gnmi_stub_converge, interfaces, - /*gnmi_timeout=*/absl::Minutes(3)); - } - - // When there is a P4Info to set, we mock a `SetForwardingPipelineConfig` - // call and a `CheckNoEntries` call. - if (p4info.has_value()) { - // Mocks a `SetForwardingPipelineConfig` call. - EXPECT_CALL(*stub, - SetForwardingPipelineConfig( - _, - EqualsProto(ConstructForwardingPipelineConfigRequest( - metadata, *p4info)), - _)) - .Times(1); - - // Mocks a `CheckNoEntries` call. - MockCheckNoEntries(*stub, *p4info); - } - } - - { - InSequence s; - // Mocks the first part of a P4RuntimeSession `Create` call. - EXPECT_CALL(mock_switch, CreateP4RuntimeStub()) - .WillOnce(Return(ByMove(std::move(stub)))); - - if (gnmi_config.has_value()) { - // Mocks the first part of a `PushGnmiConfig` call. - EXPECT_CALL(mock_switch, CreateGnmiStub) - .WillOnce(Return(ByMove(std::move(mock_gnmi_stub_push)))); - - // Mocks the first part of a `WaitForGnmiPortIdConvergence` - EXPECT_CALL(mock_switch, CreateGnmiStub) - .WillOnce(Return(ByMove(std::move(mock_gnmi_stub_converge)))); + ExpectCallToPushGnmiAndWaitForConvergence(mock_switch, interfaces); } + ExpectCallToCreateP4RuntimeSessionAndOptionallyPushP4Info(mock_switch, + p4info, metadata); } } @@ -346,7 +375,7 @@ TEST(TestHelperLibTest, for (bool push_gnmi_config : {true, false}) { for (bool push_p4info : {true, false}) { SCOPED_TRACE(absl::StrCat("push_gnmi_config: ", push_gnmi_config)); - SCOPED_TRACE(absl::StrCat("push_p4info: ", push_gnmi_config)); + SCOPED_TRACE(absl::StrCat("push_p4info: ", push_p4info)); std::optional optional_gnmi_config = push_gnmi_config ? std::make_optional(gnmi_config) : std::nullopt; std::optional optional_p4info = @@ -401,5 +430,58 @@ TEST(TestHelperLibTest, ConfigureSwitchPairAndReturnP4RuntimeSessionPair) { } } +/*** REWRITE PORT TESTS ******************************************************/ + +TEST(RewritePortsForTableEntriesTest, NoPortsInConfigFails) { + const pdpi::IrP4Info fbr_info = + sai::GetIrP4Info(sai::Instantiation::kFabricBorderRouter); + const std::string gnmi_config = EmptyOpenConfig(); + std::vector entries; + EXPECT_THAT(RewritePortsInTableEntries(fbr_info, gnmi_config, entries), + Not(IsOk())); +} + +TEST(RewritePortsForTableEntriesTest, EmptyEntriesSucceeds) { + const pdpi::IrP4Info fbr_info = + sai::GetIrP4Info(sai::Instantiation::kFabricBorderRouter); + std::vector entries; + EXPECT_OK(RewritePortsInTableEntries( + fbr_info, /*ports=*/std::vector{"1"}, entries)); +} + +TEST(RewritePortsForTableEntriesTest, GnmiConfigWorks) { + const pdpi::IrP4Info fbr_info = + sai::GetIrP4Info(sai::Instantiation::kFabricBorderRouter); + const std::string gnmi_config = OpenConfigWithInterfaces( + GnmiFieldType::kConfig, {OpenConfigInterfaceDescription{ + .port_name = "Ethernet0", + .port_id = 1, + }}); + pdpi::IrTableEntry original_entry = + gutil::ParseProtoOrDie(R"pb( + table_name: "router_interface_table" + matches { + name: "router_interface_id" + exact { str: "router-interface-1" } + } + action { + name: "set_port_and_src_mac" + params { + name: "port" + value { str: "2" } + } + params { + name: "src_mac" + value { mac: "00:02:03:04:05:06" } + } + } + )pb"); + + std::vector entries = {original_entry}; + ASSERT_OK(RewritePortsInTableEntries(fbr_info, gnmi_config, entries)); + + ASSERT_EQ(entries[0].action().params(0).value().str(), "1"); +} + } // namespace } // namespace pins_test diff --git a/tests/thinkit_gnmi_interface_util.cc b/tests/thinkit_gnmi_interface_util.cc index 4d6bd56bd..1edb82559 100644 --- a/tests/thinkit_gnmi_interface_util.cc +++ b/tests/thinkit_gnmi_interface_util.cc @@ -417,6 +417,7 @@ absl::StatusOr IsCopperPort(gnmi::gNMI::StubInterface* sut_gnmi_stub, absl::StatusOr GenerateInterfaceBreakoutConfig( absl::string_view port, const int id, absl::string_view breakout_speed, const bool is_copper_port) { + auto interface_config = absl::Substitute( R"pb({ "config": { diff --git a/tests/thinkit_gnmi_interface_util_tests.cc b/tests/thinkit_gnmi_interface_util_tests.cc index 0a9baa9f9..60f8f5fb8 100644 --- a/tests/thinkit_gnmi_interface_util_tests.cc +++ b/tests/thinkit_gnmi_interface_util_tests.cc @@ -1867,4 +1867,5 @@ TEST_F(GNMIThinkitInterfaceUtilityTest, StatusIs(absl::StatusCode::kInternal, HasSubstr("Failed to convert string (XYZ) to float"))); } + } // namespace pins_test