This document is for developers that wish to write a unit test for bmcweb.
This document should cover all the libraries that are used when writing a unit test for bmcweb.
If you already know how to run unit tests locally, please skip this section.
Otherwise please read the “Docker native builds and unit tests” section in README_GOOGLE.md
Please take a look at this example commit to follow along.
These are the following steps to write a gbmcweb unit test for GET.
If I am testing a handler in redfish-core/lib/managers.hpp, and there is no test file, then please create one. We are creating a corresponding test file per redfish-core library file.
If you do create a file, please add the following file path to the test file in srcfiles_unittest list in the meson.build. This adds the new unit test file to the meson build and will automatically test it in all future presubmits.
If the handler you are unit testing looks something like below, you can skip this step.
BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/") .privileges(redfish::privileges::getChassis) .methods(boost::beast::http::verb::get)( std::bind_front(handleChassisGet, std::ref(app)));
Otherwise if your handler has an explicit lambda function defined instead of a reference to a method call like below, please refactor the code such that the handler has its own function. This is so that we can maximize the unit test coverage for this handler.
Here is an example commit that includes this refactoring. (Yes this is not a GET handler but this applies to all unit tests)
All gBMCWeb unit tests will be either using or inheriting from the test fixture defined in snapshot_fixture.hpp. This is because this class has the necessary setup required to deserialize managedStore.
Starting your test with “TEST_F(SnapshotFixture, $NAME_OF_TEST)” is enough.
Example call:
unitTestHandlerFunction(app_, CreateRequest(), share_async_resp_);
The call above is the most basic handler call. Some unit tests may require extra parameters to be passed in, but those should be URL ids such as system or chassis name.
Note: app_and share_async_resp_ are class variables of SnapshotFixture
Note: CreateRequest() creates a crow::Request for the handler. For the purposes of GET requests, no parameters need to be passed in
In order to catch dangling references passed to DBus callbacks in unit tests, we actually run the DBus call passed into the managed store in an io context instead of triggering the callback immediately.
This means the io context must be run in order for the response to be populated correctly. This can be done with a simple method call.
// Perform dbus calls RunIoUntilDone();
We can now use EXPECT_EQ statements to test the response object. Sample expect statements are provided below:
// Test properties of share_async_resp_->res.jsonValue EXPECT_EQ(json["@odata.id"], "/redfish/v1/Chassis"); EXPECT_EQ(json["@odata.type"], "#ChassisCollection.ChassisCollection"); EXPECT_EQ(json["Name"], "Chassis Collection"); // Ensure response is OK EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
If you are missing DBus objects, you have two options.
This option is recommended if you are missing a lot of DBus objects from the existing snapshots. If it would be easier to just take an entirely new snapshot instead of mocking one or two DBus objects, then please follow the steps below on “How to take a new snapshot”
Usually this should be the preferred method if a handler is meant for a certain platform which there is no current existing snapshot of.
This is directly creating the DBus object you want to test for and manually inserting it into the managedStore. Here is an example.
Take a look at this example commit and follow along.
We want to test the URI “/redfish/v1/Systems//LogServices/FaultLog/Entries”.
This makes one DBus call that looks like the following:
managedStore::GetManagedObjectStore()->getManagedObjectsWithContext( "xyz.openbmc_project.Dump.Manager", {"/xyz/openbmc_project/dump"}...);
You can see that this DBus call is made in the test logs. If you invoke this line in the unit test and check the logs at “build/meson-logs/testlog.txt” (I use build for my meson build artifacts).
You will see the following lines which reference what key was used to search managedStore for the DBus call. This specific DBus call has a log like this.
Object found in store. Key: kManagedObject|xyz.openbmc_project.Dump.Manager|/xyz/openbmc_project/dump
This will happen if it finds an object in the store.
kManagedObject|xyz.openbmc_project.Dump.Manager|/xyz/openbmc_project/dump1 not found in cache.
If it does not, it should also print what key it tried.
Regardless, you should be able to find the full key used to search the snapshot and with this information you can either add or update the value using upsertMockObjectIntoManagedStore
as seen in the commit above.
To create the actual value to be inserted into managedStore, we utilize CreateMockFaultLogEntry
to mock the DBus object to be inserted.
NOTE: test/g3/mock_managed_store_test.cpp:258 has examples on creation of fake DBus objects.
With all these steps complete, the DBus call you were trying to mock should just now return what you upserted into the managedStore.
So in total, the steps will be:
upsertMockObjectIntoManagedStore
to update/insert the object in managedStoreNote: Several other methods have been included to interface managedStore.
evictMockObjectFromManagedStore
: This will remove a key value pair from managedStore
getMockObjectFromManagedStore
: This will get the corresponding value from a KeyType in managedStore
printObjectFromManagedStore
: This will log the corresponding value from the KeyType for debugging.
Please take a look at this example commit to follow along.
These are the following steps to write a gbmcweb unit test for GET/POST/PATCH/DELETE
This is the exact same step 1 as above. Please refer to Step #1 in “Writing Unit Tests for GET Handlers”.
This is the exact same step 2 as above. Please refer to Step #2 in “Writing Unit Tests for GET Handlers”.
We are going to create an EXPECT_CALL for each Dbus call made. To see how to set these up, we will take a look at the example commit above.
Specifically in handleDeleteEventLogEntry
, the following DBus call is made
managedStore::GetManagedObjectStore()->PostDbusCallToIoContextThreadSafe( strand, respHandler, "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging/entry/" + entryID, "xyz.openbmc_project.Object.Delete", "Delete");
Calls to PostDbusCallToIoContextThreadSafe
must have an associating EXPECT_CALL in order to properly mock the behavior in the unit test.
The following EXPECT_CALL would work for the above file. (Assume entryId is “entry1”)
// Simulate a Failed DBus Call // Setup dbus mocking EXPECT_CALL(*managedStore::GetManagedObjectStore(), PostDbusCallToIoContextThreadSafe(An<absl::AnyInvocable<void(const boost::system::error_code&)>&&>(), "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging/entry/entry1", "xyz.openbmc_project.Object.Delete", "Delete")) .Times(1) .WillOnce(SimulateFailedAsyncPostDbusCallAction:: SimulateFailedAsyncPostDbusCall());
//Simulate a Succesful DBus Call EXPECT_CALL(*managedStore::GetManagedObjectStore(), PostDbusCallToIoContextThreadSafe(An<absl::AnyInvocable<void(const boost::system::error_code&)>&&>(), "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging/entry/entry1", "xyz.openbmc_project.Object.Delete", "Delete")) .Times(1) .WillOnce(SimulateSuccessfulAsyncPostDbusCallAction:: SimulateSuccessfulAsyncPostDbusCall());
Things to note:
- We are expecting the global managedStore object to perform the DBus call. This is marked by
*managedStore::GetManagedObjectStore()
as >the first parameter- We must pass in all parameters expected in the DBus call in order to properly mock it.
- The callback must actually use
An<absl::AnyInvocable<..>>
An
can be brought in using ::testing::An.- .Times() shows the amount of times we expect this call to occur
- The defining action can be one of the following:
- SimulateFailedAsyncPostDbusCallAction::SimulateFailedAsyncPostDbusCall()
- Calls the callback with ec != 0. (An error occurred during Dbus call)
- SimulateSuccessfulAsyncPostDbusCallAction::SimulateSuccessfulAsyncPostDbusCall()
- Calls callback with ec ==0. (Successful DBus call)
This is similar to step 3 above with one big note.
Methods like POST and PATCH require json data that need to be sent. This can actually be done using CreateRequest. If I wanted to simulate the following curl request below
curl -X PATCH http://localhost/redfish/v1/Systems/system/LogServices/EventLog/Entries/entry1 -d '{"ResetType":"ForceOff"}'
This can be simulated by calling the handler in the following way:
handlePatchEventLogEntry(app_, CreateRequest("{\"Resolved\": true }"), share_async_resp_, "system", "entry1");
The json string can be directly passed into CreateRequest to simulate the data being sent.
In order to catch dangling references passed to DBus callbacks in unit tests, we actually run the DBus call passed into PostDbusCallToIoContextThreadSafe
in an io context instead of triggering the callback immediately.
This means the io context must be run in order for the response to be populated correctly. This can be done with a simple method call.
// Perform dbus calls RunIoUntilDone();
This is the same as above. We are just validating the response object.
There will be situations where the current snapshot is not enough and you need a new snapshot for your own special platform or configuration. The steps to perform this are very simple.
Please follow the “How to take a new snapshot” section at go/gbmcweb-unit-testing-playbook.
Please be ABSOLUTELY sure that you are not uploading sensitive information to this public repo. If you need help please reach out to edwarddl@ to see if your snapshot can be uploaded or not.
You must be able to ssh into the machine as the file is downloaded in /tmp/
Run the following command on the BMC
curl -X POST \ 127.0.0.1/redfish/v1/Managers/bmc/ManagerDiagnosticData/Oem/Google/GoogleSnapShot/Actions/GoogleTakeSnapShot cat /tmp/gBMCwebManagedStore.json | jq > $SNAPSHOT_FILE_NAME
Note: If you are getting an empty snapshot, that is most likely because you need to warm the cache. A quick BFS through all of the redfish URLs should warm the cache and after retrying these steps again, you should have a full serialization.
You can add the snapshot to the folder “test/snapshots/”.
Please also update the readme file.
You can create a new meson option to hardcode the snapshot filepath.
Please note that if you would like the presubmit to pass using your new snapshot, you will need to include the ci_workspace in the filepath.
For example, if my snapshot is located in test/snapshots/snapshot1.json, the default filepath of the snapshot should be /tmpfs/src/ci_workspace/gbmcweb/test/snapshots/snapshot1.json
.
You can now create a snapshot fixture that deserializes the json at the filepath you designed in Step #4. Then you can proceed to write the unit test as normal.