blob: 85cb71fd23dd104d4556cf9b4491d59866603f61 [file] [edit]
/*
* SPDX-FileCopyrightText: Copyright OpenBMC Authors
* SPDX-License-Identifier: Apache-2.0
*/
#include "Inventory.hpp"
#include "MctpMockTestBase.hpp"
#include "MessagePackUnpackUtils.hpp"
#include "MockMctpRequester.hpp"
#include "NvidiaGpuControlErrors.hpp"
#include "NvidiaGpuMctpVdm.hpp"
#include "NvidiaGpuPowerControl.hpp"
#include "OcpMctpVdm.hpp"
#include "TestUtils.hpp"
#include <sdbusplus/asio/object_server.hpp>
#include <sdbusplus/exception.hpp>
#include <chrono>
#include <cstdint>
#include <limits>
#include <memory>
#include <span>
#include <string>
#include <system_error>
#include <tuple>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace
{
constexpr uint32_t milliwattsPerWatt = 1000;
constexpr const char* powerCapIfaceName =
"xyz.openbmc_project.Control.Power.Cap";
// Power-cap window the stub device reports through the inventory. The
// PowerCap setter accepts values inside it and rejects everything else.
constexpr uint32_t minCapWatts = 100;
constexpr uint32_t maxCapWatts = 500;
// Value seeded by a successful read before each error case, in milliwatts.
constexpr uint32_t seededOneshotMw = 300000;
// Comfortably longer than the control's 100 ms debounce window.
constexpr std::chrono::seconds debounceTimeout{5};
// Long enough that a second debounce window would have fired had the burst
// not been coalesced.
constexpr std::chrono::seconds quietWindow{1};
using Association = std::tuple<std::string, std::string, std::string>;
// Build a successful GET_POWER_LIMITS response:
// CommonResponse header + 3 x uint32_t (persistent, oneshot, enforced)
std::vector<uint8_t> buildPowerLimitsResponse(
uint32_t persistentMw, uint32_t oneshotMw, uint32_t enforcedMw)
{
const uint16_t dataSize = sizeof(uint32_t) * 3;
std::vector<uint8_t> buf(
ocp::accelerator_management::commonResponseSize + dataSize);
PackBuffer pack(buf);
ocp::accelerator_management::packHeader(
pack, gpu::nvidiaPciVendorId,
ocp::accelerator_management::MessageType::RESPONSE, 0,
static_cast<uint8_t>(gpu::MessageType::PLATFORM_ENVIRONMENTAL));
pack.pack(static_cast<uint8_t>(
gpu::PlatformEnvironmentalCommands::GET_POWER_LIMITS));
pack.pack(static_cast<uint8_t>(
ocp::accelerator_management::CompletionCode::SUCCESS));
pack.pack(static_cast<uint16_t>(0)); // reasonCode
pack.pack(dataSize);
pack.pack(persistentMw);
pack.pack(oneshotMw);
pack.pack(enforcedMw);
EXPECT_EQ(pack.getError(), 0);
return buf;
}
// Build a successful GET_INVENTORY_INFORMATION response whose payload starts
// with `leadingValue`. A 16-byte payload decodes cleanly for every property
// the Inventory asks for, and the power-limit properties consume the leading
// uint32.
std::vector<uint8_t> buildInventoryResponse(uint32_t leadingValue)
{
constexpr uint16_t dataSize = 16;
std::vector<uint8_t> buf(
ocp::accelerator_management::commonResponseSize + dataSize, 0);
PackBuffer pack(buf);
ocp::accelerator_management::packHeader(
pack, gpu::nvidiaPciVendorId,
ocp::accelerator_management::MessageType::RESPONSE, 0,
static_cast<uint8_t>(gpu::MessageType::PLATFORM_ENVIRONMENTAL));
pack.pack(static_cast<uint8_t>(
gpu::PlatformEnvironmentalCommands::GET_INVENTORY_INFORMATION));
pack.pack(static_cast<uint8_t>(
ocp::accelerator_management::CompletionCode::SUCCESS));
pack.pack(static_cast<uint16_t>(0)); // reasonCode
pack.pack(dataSize);
pack.pack(leadingValue);
EXPECT_EQ(pack.getError(), 0);
return buf;
}
std::vector<uint8_t> buildErrorResponse()
{
return test_utils::buildPlatformEnvErrorResponse(
gpu::PlatformEnvironmentalCommands::GET_POWER_LIMITS,
static_cast<uint8_t>(
ocp::accelerator_management::CompletionCode::ERROR),
0x1234);
}
// Command byte of a PLATFORM_ENVIRONMENTAL request; it directly follows the
// message header.
uint8_t requestCommand(const std::vector<uint8_t>& request)
{
return request.size() > ocp::accelerator_management::messageHeaderSize
? request[ocp::accelerator_management::messageHeaderSize]
: 0;
}
// Property id of a GetInventoryInformation request; it is the last byte.
uint8_t inventoryPropertyId(const std::vector<uint8_t>& request)
{
return request.empty() ? 0 : request.back();
}
// Payload of a SetPowerLimits request, after the common header:
// powerLimitId(4) + action(1) + persistence(1) + milliwatts(4)
struct SetPowerLimitsRequest
{
uint8_t action{};
uint8_t persistence{};
uint32_t milliwatts{};
};
SetPowerLimitsRequest decodeSetPowerLimits(const std::vector<uint8_t>& request)
{
UnpackBuffer unpack(request);
ocp::accelerator_management::MessageType ocpMsgType{};
uint8_t instanceId = 0;
uint8_t msgType = 0;
EXPECT_EQ(ocp::accelerator_management::unpackHeader(
unpack, gpu::nvidiaPciVendorId, ocpMsgType, instanceId,
msgType),
0);
uint8_t command = 0;
uint8_t dataSize = 0;
uint32_t powerLimitId = 0;
SetPowerLimitsRequest decoded;
unpack.unpack(command);
unpack.unpack(dataSize);
unpack.unpack(powerLimitId);
unpack.unpack(decoded.action);
unpack.unpack(decoded.persistence);
unpack.unpack(decoded.milliwatts);
EXPECT_EQ(unpack.getError(), 0);
return decoded;
}
class NvidiaGpuPowerControlTest : public MctpMockTestBase
{
protected:
// The control registers PowerCap/PowerCapEnable itself and initializes
// the interface, so mirror GpuDevice: register only the static limits.
// The interface is kept on the fixture because the two setters are
// private to the control, so the tests drive them through D-Bus.
void makePowerCapInterface(const std::string& name)
{
powerCapInterface =
objects().add_interface(powerCapPath(name), powerCapIfaceName);
powerCapInterface->register_property("MinPowerCapValue", uint32_t{0});
powerCapInterface->register_property(
"MaxPowerCapValue", std::numeric_limits<uint32_t>::max());
powerCapInterface->register_property(
"DefaultPowerCap", std::numeric_limits<uint32_t>::max());
}
std::shared_ptr<NvidiaGpuPowerControl> createControl(
const std::string& name = "GPU_CTRL",
uint8_t eid = test_utils::defaultEid)
{
makePowerCapInterface(name);
return std::make_shared<NvidiaGpuPowerControl>(
objects(), name, requester(), eid, ioContext(), powerCapInterface,
nullptr);
}
// GpuDevice hands one Power.Cap interface to both the Inventory and the
// control; the Inventory is what supplies the [min, max] window the
// PowerCap setter range-checks against. `fetchLimits` selects whether
// that window has already been read from the device.
std::shared_ptr<NvidiaGpuPowerControl> createControlWithInventory(
const std::string& name, bool fetchLimits)
{
makePowerCapInterface(name);
inventory = std::make_shared<Inventory>(
bus(), objects(), name, requester(),
gpu::DeviceIdentification::DEVICE_GPU, test_utils::defaultEid,
ioContext(), powerCapInterface, nullptr);
auto ctrl = std::make_shared<NvidiaGpuPowerControl>(
objects(), name, requester(), test_utils::defaultEid, ioContext(),
powerCapInterface, inventory);
if (fetchLimits)
{
inventory->init();
}
return ctrl;
}
// Record every request and answer inventory queries with the stub
// device's power-limit window. Anything else gets an empty buffer, which
// the caller treats as an undecodable response: these tests assert on
// what was sent, not on what came back.
void installDeviceResponder()
{
ON_CALL(mctpMock, sendRecvMsg)
.WillByDefault([this](uint8_t /*eid*/,
std::span<const uint8_t> reqMsg,
auto callback) {
const std::vector<uint8_t>& request =
requests.emplace_back(reqMsg.begin(), reqMsg.end());
if (requestCommand(request) !=
static_cast<uint8_t>(gpu::PlatformEnvironmentalCommands::
GET_INVENTORY_INFORMATION))
{
callback(std::error_code{}, std::span<const uint8_t>{});
return;
}
uint32_t value = 0;
if (inventoryPropertyId(request) ==
static_cast<uint8_t>(
gpu::InventoryPropertyId::MIN_DEVICE_POWER_LIMIT))
{
value = minCapWatts * milliwattsPerWatt;
}
else if (inventoryPropertyId(request) ==
static_cast<uint8_t>(
gpu::InventoryPropertyId::MAX_DEVICE_POWER_LIMIT))
{
value = maxCapWatts * milliwattsPerWatt;
}
const std::vector<uint8_t> response =
buildInventoryResponse(value);
callback(std::error_code{}, response);
});
}
// The SetPowerLimits requests recorded by installDeviceResponder().
std::vector<std::vector<uint8_t>> setPowerLimitRequests() const
{
std::vector<std::vector<uint8_t>> matches;
for (const std::vector<uint8_t>& request : requests)
{
if (requestCommand(request) ==
static_cast<uint8_t>(
gpu::PlatformEnvironmentalCommands::SET_POWER_LIMITS))
{
matches.push_back(request);
}
}
return matches;
}
static std::string powerCapPath(const std::string& name)
{
return "/xyz/openbmc_project/control/power/" + name + "_iface";
}
// The D-Bus view of the cap; asserting it after a failed read is how the
// error cases show the last good values survived.
static void expectPowerCap(const std::string& name, uint32_t watts,
bool enabled)
{
EXPECT_EQ(getProperty<uint32_t>(powerCapPath(name), powerCapIfaceName,
"PowerCap"),
watts);
EXPECT_EQ(getProperty<bool>(powerCapPath(name), powerCapIfaceName,
"PowerCapEnable"),
enabled);
}
std::shared_ptr<sdbusplus::asio::dbus_interface> powerCapInterface;
std::shared_ptr<Inventory> inventory;
std::vector<std::vector<uint8_t>> requests;
};
// ── Constructor ─────────────────────────────────────────────────────
TEST_F(NvidiaGpuPowerControlTest, ConstructorDoesNotCrash)
{
auto ctrl = createControl("ctrl_ctor");
ASSERT_NE(ctrl, nullptr);
}
// ── Update (success path) ───────────────────────────────────────────
TEST_F(NvidiaGpuPowerControlTest, UpdateSuccessSetsProperties)
{
// PowerCap mirrors the one-shot limit; the flag is enabled only when
// the one-shot limit is the one being enforced.
constexpr uint32_t oneshotMw = 300000; // 300W
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{}, buildPowerLimitsResponse(400000, oneshotMw, oneshotMw)));
auto ctrl = createControl("ctrl_succ");
ctrl->update();
// Verify the D-Bus properties are set
expectPowerCap("ctrl_succ", oneshotMw / milliwattsPerWatt, true);
}
TEST_F(NvidiaGpuPowerControlTest, UpdateUnlimitedPowerCapDisablesFlag)
{
// enforced = max uint32 → PowerCapEnable=false
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{}, buildPowerLimitsResponse(
0, 0, std::numeric_limits<uint32_t>::max())));
auto ctrl = createControl("ctrl_unlim");
ctrl->update();
EXPECT_FALSE(getProperty<bool>(powerCapPath("ctrl_unlim"),
powerCapIfaceName, "PowerCapEnable"));
}
TEST_F(NvidiaGpuPowerControlTest, UpdateEnforcedMismatchDisablesFlag)
{
// enforced != one-shot → PowerCapEnable=false
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{}, buildPowerLimitsResponse(400000, 350000, 300000)));
auto ctrl = createControl("ctrl_mismatch");
ctrl->update();
EXPECT_FALSE(getProperty<bool>(powerCapPath("ctrl_mismatch"),
powerCapIfaceName, "PowerCapEnable"));
}
// ── Update (sends request) ──────────────────────────────────────────
TEST_F(NvidiaGpuPowerControlTest, UpdateSendsRequest)
{
EXPECT_CALL(mctpMock, sendRecvMsg)
.Times(testing::AtLeast(1))
.WillRepeatedly(mock_mctp::respondWith({}, {}));
auto ctrl = createControl("ctrl_sends");
ctrl->update();
}
TEST_F(NvidiaGpuPowerControlTest, UpdateRequestContainsCorrectEid)
{
constexpr uint8_t testEid = 42;
EXPECT_CALL(mctpMock, sendRecvMsg(testEid, testing::_, testing::_))
.WillOnce(mock_mctp::respondWith({}, {}));
auto ctrl = createControl("ctrl_eid", testEid);
ctrl->update();
}
TEST_F(NvidiaGpuPowerControlTest, UpdateVerifiesRequestEncoding)
{
// Copy the request bytes before completing the call: the reqMsg span is
// a view into caller-owned memory, valid only during the call.
std::vector<uint8_t> lastRequest;
const std::vector<uint8_t> response;
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce([&](uint8_t /*eid*/, std::span<const uint8_t> reqMsg,
auto callback) {
lastRequest.assign(reqMsg.begin(), reqMsg.end());
callback(std::error_code{}, response);
});
auto ctrl = createControl("ctrl_enc");
ctrl->update();
ASSERT_FALSE(lastRequest.empty());
UnpackBuffer unpack(lastRequest);
ocp::accelerator_management::MessageType ocpMsgType{};
uint8_t instanceId = 0;
uint8_t msgType = 0;
const int rc = ocp::accelerator_management::unpackHeader(
unpack, gpu::nvidiaPciVendorId, ocpMsgType, instanceId, msgType);
EXPECT_EQ(rc, 0);
EXPECT_EQ(ocpMsgType, ocp::accelerator_management::MessageType::REQUEST);
EXPECT_EQ(msgType,
static_cast<uint8_t>(gpu::MessageType::PLATFORM_ENVIRONMENTAL));
uint8_t command = 0;
unpack.unpack(command);
EXPECT_EQ(command,
static_cast<uint8_t>(
gpu::PlatformEnvironmentalCommands::GET_POWER_LIMITS));
EXPECT_EQ(unpack.getError(), 0);
}
// ── PowerCap set (range checks) ─────────────────────────────────────
TEST_F(NvidiaGpuPowerControlTest, PowerCapSetWithoutInventoryIsRejected)
{
auto ctrl = createControl("ctrl_no_inv");
// Without an Inventory the accepted window is unknown, so the setter has
// to refuse rather than forward an unvalidated cap to the device.
EXPECT_THROW(powerCapInterface->set_property("PowerCap", uint32_t{200}),
Unavailable);
}
TEST_F(NvidiaGpuPowerControlTest, PowerCapSetBeforeLimitsAreKnownIsRejected)
{
// The Inventory exists but has not read the device's limits yet.
auto ctrl = createControlWithInventory("ctrl_no_limits", false);
EXPECT_THROW(powerCapInterface->set_property("PowerCap", uint32_t{200}),
Unavailable);
}
TEST_F(NvidiaGpuPowerControlTest, PowerCapSetOutsideDeviceWindowIsRejected)
{
installDeviceResponder();
auto ctrl = createControlWithInventory("ctrl_range", true);
// InvalidArgument rather than Unavailable proves the window itself was
// known and the value is what failed.
EXPECT_THROW(powerCapInterface->set_property("PowerCap", minCapWatts - 1),
InvalidArgument);
EXPECT_THROW(powerCapInterface->set_property("PowerCap", maxCapWatts + 1),
InvalidArgument);
// A rejected cap must never reach the device, not even after the
// debounce window a successful write would have used.
EXPECT_FALSE(pumpIoUntil(
[this] { return !setPowerLimitRequests().empty(); }, quietWindow));
}
// ── PowerCap set (debounce coalescing) ──────────────────────────────
TEST_F(NvidiaGpuPowerControlTest, CapAndEnableWritesCoalesceIntoOneRequest)
{
installDeviceResponder();
auto ctrl = createControlWithInventory("ctrl_debounce", true);
// A PATCH of both properties arrives as two back-to-back writes. Each one
// re-arms the debounce timer, so the burst has to produce a single
// SetPowerLimits carrying the cap from the first write and the
// NEW_LIMIT action implied by the second.
ASSERT_NO_THROW(powerCapInterface->set_property("PowerCap", maxCapWatts));
ASSERT_NO_THROW(powerCapInterface->set_property("PowerCapEnable", true));
ASSERT_TRUE(pumpIoUntil([this] { return !setPowerLimitRequests().empty(); },
debounceTimeout));
EXPECT_FALSE(pumpIoUntil(
[this] { return setPowerLimitRequests().size() > 1; }, quietWindow));
const std::vector<std::vector<uint8_t>> sets = setPowerLimitRequests();
ASSERT_EQ(sets.size(), 1U);
const SetPowerLimitsRequest decoded = decodeSetPowerLimits(sets.front());
EXPECT_EQ(decoded.milliwatts, maxCapWatts * milliwattsPerWatt);
EXPECT_EQ(decoded.action,
static_cast<uint8_t>(gpu::SetPowerLimitsAction::NEW_LIMIT));
EXPECT_EQ(decoded.persistence,
static_cast<uint8_t>(gpu::SetPowerLimitsPersistence::ONE_SHOT));
}
// ── Error handling ──────────────────────────────────────────────────
//
// Each failure path seeds a good reading first, so the assertion is that the
// failure left the previously decoded values in place.
TEST_F(NvidiaGpuPowerControlTest, UpdateMctpTransportErrorKeepsPreviousValues)
{
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{},
buildPowerLimitsResponse(400000, seededOneshotMw, seededOneshotMw)))
.WillOnce(mock_mctp::respondWith(
std::make_error_code(std::errc::timed_out), {}));
auto ctrl = createControl("ctrl_mctp_err");
ctrl->update();
ctrl->update();
expectPowerCap("ctrl_mctp_err", seededOneshotMw / milliwattsPerWatt, true);
}
TEST_F(NvidiaGpuPowerControlTest, UpdateDecodeErrorKeepsPreviousValues)
{
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{},
buildPowerLimitsResponse(400000, seededOneshotMw, seededOneshotMw)))
.WillOnce(mock_mctp::respondWith({}, buildErrorResponse()));
auto ctrl = createControl("ctrl_dec_err");
ctrl->update();
ctrl->update();
expectPowerCap("ctrl_dec_err", seededOneshotMw / milliwattsPerWatt, true);
}
TEST_F(NvidiaGpuPowerControlTest, UpdateEmptyBufferKeepsPreviousValues)
{
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{},
buildPowerLimitsResponse(400000, seededOneshotMw, seededOneshotMw)))
.WillOnce(mock_mctp::respondWith({}, {}));
auto ctrl = createControl("ctrl_empty");
ctrl->update();
ctrl->update();
expectPowerCap("ctrl_empty", seededOneshotMw / milliwattsPerWatt, true);
}
TEST_F(NvidiaGpuPowerControlTest, UpdateTinyBufferKeepsPreviousValues)
{
EXPECT_CALL(mctpMock, sendRecvMsg)
.WillOnce(mock_mctp::respondWith(
{},
buildPowerLimitsResponse(400000, seededOneshotMw, seededOneshotMw)))
.WillOnce(mock_mctp::respondWith({}, {0x00, 0x01}));
auto ctrl = createControl("ctrl_tiny");
ctrl->update();
ctrl->update();
expectPowerCap("ctrl_tiny", seededOneshotMw / milliwattsPerWatt, true);
}
// ── Destructor ──────────────────────────────────────────────────────
TEST_F(NvidiaGpuPowerControlTest, DestructorRemovesInterface)
{
const std::string name = "ctrl_dtor";
const std::string assocPath = "/xyz/openbmc_project/control/power/" + name;
{
auto ctrl = createControl(name);
ASSERT_NE(ctrl, nullptr);
// While alive, the association interface is reachable.
EXPECT_NO_THROW(getProperty<std::vector<Association>>(
assocPath, "xyz.openbmc_project.Association.Definitions",
"Associations"));
}
drainPendingAsync();
// After destruction, the property read must fail.
EXPECT_THROW(getProperty<std::vector<Association>>(
assocPath, "xyz.openbmc_project.Association.Definitions",
"Associations"),
sdbusplus::exception_t);
}
} // namespace