This document contains a codelab for debugging with diagnostics and tests. It is currently intended for developers writing tests within fuchsia.git.
Prerequisites
Set up your development environment.
This codelab assumes you have completed Getting Started and have:
- A checked out and built Fuchsia tree.
- A device or emulator (
ffx emu
) that runs Fuchsia. - A workstation to serve components (
fx serve
) to your Fuchsia device or emulator.
To build and run the examples in this codelab, add the following arguments
to your fx set
invocation:
fx set core.x64 \
--release \
--with //examples/diagnostics/workshop \
--with //examples/diagnostics/workshop:tests
Introduction
There is an example component that serves a protocol called ProfileStore:
@discoverable
closed protocol ProfileStore {
strict Open(resource struct {
key string:KEY_LENGTH;
channel server_end:Profile;
});
strict OpenReader(resource struct {
key string:KEY_LENGTH;
channel server_end:ProfileReader;
});
strict Delete(struct {
key string:KEY_LENGTH;
}) -> (struct {
success bool;
});
strict CreateOrOpen(resource struct {
key string:KEY_LENGTH;
channel server_end:Profile;
});
};
This protocol allows creation, deletion, and inspection of user profiles, which contain a name and balance. The component has a bug - profile deletion does not work.
Code for the codelab is located in //examples/diagnostics/workshop.
Run the component
In addition to the main component serving ProfileStore, there are a number of components that
connect to and interact with ProfileStore. All components are in the
fuchsia-pkg://fuchsia.com/profile_store_example
package.
#meta/profile_store.cm
- serves ProfileStore#meta/add_olive.cm
- Connects to ProfileStore and adds a profile called 'Olive'#meta/add_balance_olive.cm
- Connects to ProfileStore and adds balance to the 'Olive' profile#meta/withdraw_balance_olive.cm
- Connects to ProfileStore and withdraws balance from the 'Olive' profile#meta/add_jane.cm
- Connects to ProfileStore and adds a profile called 'Jane'#meta/delete_olive.cm
- Connects to ProfileStore and deletes the 'Olive' profile
Capabilities are routed by the #meta/laboratory_server.cm
component.
You can interact with the components using the ffx component
command, and inspect output from
components using ffx log
.
First, run ffx log --tags workshop
in a shell. This shell will contain all output from
components. In a different shell, run the toy components:
# setup server
ffx component create /core/ffx-laboratory:profile_store fuchsia-pkg://fuchsia.com/profile_store_example#meta/laboratory_server.cm
# setup first client
ffx component create /core/ffx-laboratory:profile_store/clients:add_olive fuchsia-pkg://fuchsia.com/profile_store_example#meta/add_olive.cm
# see the results of the previous two steps
ffx component show profile_store
# add a profile key and read it
ffx component start /core/ffx-laboratory:profile_store/clients:add_olive
ffx component create /core/ffx-laboratory:profile_store/clients:reader fuchsia-pkg://fuchsia.com/profile_store_example#meta/profile_reader.cm
ffx component start /core/ffx-laboratory:profile_store/clients:reader
# demonstrate persistence
ffx component stop /core/ffx-laboratory:profile_store/profile_store
ffx component start /core/ffx-laboratory:profile_store/clients:reader
# update balance
ffx component create /core/ffx-laboratory:profile_store/clients:add_balance_olive fuchsia-pkg://fuchsia.com/profile_store_example#meta/add_balance_olive.cm
ffx component start /core/ffx-laboratory:profile_store/clients:add_balance_olive
ffx component start /core/ffx-laboratory:profile_store/clients:reader
# add second profile
ffx component create /core/ffx-laboratory:profile_store/clients:add_jane fuchsia-pkg://fuchsia.com/profile_store_example#meta/add_jane.cm
ffx component start /core/ffx-laboratory:profile_store/clients:add_jane
ffx component start /core/ffx-laboratory:profile_store/clients:reader
# update balance
ffx component create /core/ffx-laboratory:profile_store/clients:withdraw_balance_olive fuchsia-pkg://fuchsia.com/profile_store_example#meta/withdraw_balance_olive.cm
ffx component start /core/ffx-laboratory:profile_store/clients:withdraw_balance_olive
ffx component start /core/ffx-laboratory:profile_store/clients:reader
# delete olive (this will not work as there is a bug in the server code)
ffx component create /core/ffx-laboratory:profile_store/clients:delete_olive fuchsia-pkg://fuchsia.com/profile_store_example#meta/delete_olive.cm
ffx component start /core/ffx-laboratory:profile_store/clients:delete_olive
ffx component start /core/ffx-laboratory:profile_store/clients:reader
Debugging with diagnostics
Diagnostics provides multiple products that help component authors debug their components both while developing and in the field.
For this workshop we'll be exploring three core technologies:
Structured logging
Diagnostics provides structured logging libraries to allow components to write logs. To help find the bug, we'll be adding a few logs to the profile store component.
The first step when adding logging to a component, is to include the logging library in your binary dependencies. To do this, update your BUILD.gn as follows:
source_set("lib") {
...
public_deps = [
...
"//sdk/lib/syslog/cpp",
]
}
Logging is initialized the moment we call one of the logging macros. However, the libraries provide
some utilities that should be called in main()
such as configuring the tags (if desired only, this
is optional).
Tags can be useful to later query the logs of a group of components. For our purposes we can add
the workshop
tag:
#include <lib/syslog/cpp/log_settings.h>
...
fuchsia_logging::SetTags({"workshop", "profile_store_server"});
Now, it's time to write some logs. We'll be using the FX_SLOG
macro which allows to write
structured keys and values.
For example, we can add the following log when we get a request on ProfileStore::Open
but
the profile file doesn't exist:
#include <lib/syslog/cpp/macros.h>
...
FX_SLOG(WARNING, "Profile doesn't exist", KV("key", key.c_str()));
Try adding that log, build (fx build
), relaunch your component (ffx component start ...
) and
then run: ffx log --tags workshop
.
What other logs could we add that would help identify the log? Please experiment!
A solution can be found in this patch.
Inspect
Inspect allows components to expose state about themselves. Unlike logs, which are a stream, Inspect represents a live view into the component current state.
Reading through the Inspect quickstart would be a good first step. If you'd like to dive deeper into Inspect, you can also follow the Inspect codelab.
To get started, first add library dependencies:
source_set("lib") {
...
public_deps = [
...
"//sdk/lib/inspect/component/cpp",
]
}
Next, initialize Inspect in main.cc
:
#include <lib/inspect/component/cpp/component.h>
...
// Immediately following the line defining "startup".
auto inspector = std::make_unique<inspect::ComponentInspector>(async_get_default_dispatcher(), inspect::PublishOptions{});
inspector->Health().Ok();
Now you can view Inspect after restarting the profile_store
:
# Note that double \\ is necessary! ':' must be escaped in a "diagnostic selector."
ffx inspect show core/ffx-laboratory\\:profile_store/profile_store
You should see your component's status is "OK". Inspect is most
useful when integrated with your class hierarchy. Arbitrary values
are rooted on inspect::Node
, including more Nodes! Try to modify ProfileStore such that the following compiles:
// In main.cc
std::unique_ptr<ProfileStore> profile_store =
std::make_unique<ProfileStore>(loop.dispatcher(), inspector->GetRoot().CreateChild("profiles"));
Hint: You will need to update the ProfileStoreTests
class if you
change the constructor for ProfileStore
. You can just pass
inspect::Node()
as the new parameter.
Now that you have basic Inspect configured, what intrumentation could be useful to add to
help prevent/find the bug in this component?
- Consider adding an inspect::Node
to each Profile
, and create
them using CreateChild
on the node you passed to ProfileStore
.
- Consider using inspect::LazyNode
(node.CreateLazyNode(...)
)
to dynamically create hierarchies.
A possible solution can be found in this patch: https://fuchsia-review.googlesource.com/c/fuchsia/+/682671
Triage
Triage allows to write rules to automatically process inspect snapshots and find potential issues or gather stats that the snapshots might contain.
Reading through the Triage codelab would be a good first step as well as reading through the triage config guide.
To get started, create a new file at examples/diagnostics/workshop/triage/profile_store.triage
with the following contents:
{
select: {
profile_status: "INSPECT:core/ffx-laboratory\\:profile_store/profile_store:fuchsia.inspect.Health:status",
},
act: {
profile_status_ok: {
type: "Warning",
trigger: "profile_status != 'OK'",
print: "Profile store is not healthy.",
}
}
}
If you followed the Inspect quick start in the last section, run
ffx triage --config examples/diagnostics/workshop/triage/
. If profile_store
is running and
reporting its status is OK, you won't see any failures! Try changing the call to
Health().Ok()
in main.cc
to Health().StartingUp()
and run
ffx triage --config examples/diagnostics/workshop/triage/
again. This time you should see the warning.
Try writing a triage configuration that could have helped spot the bug in snapshots gathered in the field.
A possible solution (built on top of the Inspect solution) can be found in this patch: https://fuchsia-review.googlesource.com/c/fuchsia/+/684762
Verifying with tests
This section covers adding tests to verify the fix.
This example contains example unit tests and an example integration test, including some tests that are disabled due to the bug.
Try writing new tests that could help prevent the bug in the component. Feel free to either modify the example tests, or create new tests from scratch using the flows below.
Adding new unit tests
The code structure of unit tests is highly dependent on the runtime used. This section walks through the process of setting up new unit tests that verify the behavior of the ProfileStore C++ class.
Create a new file in examples/diagnostics/workshop/profile_store_unittest.cc
with these contents:
#include "src/lib/testing/loop_fixture/test_loop_fixture.h"
#include <gtest/gtest.h>
#include "fuchsia/examples/diagnostics/cpp/fidl.h"
#include "lib/fidl/cpp/interface_ptr.h"
#include "profile_store.h"
class ProfileStoreTests : public gtest::TestLoopFixture {};
TEST_F(ProfileStoreTests, SampleTest) {
ProfileStore store(dispatcher());
fidl::InterfacePtr<fuchsia::examples::diagnostics::ProfileStore> store_ptr;
store.AddBinding(store_ptr.NewRequest(dispatcher()));
store_ptr->Delete("my_key", [&](bool successful) { EXPECT_FALSE(successful); });
RunLoopUntilIdle();
}
This sets up a minimal unit test which creates a ProfileStore, and creates a client to interact
with it under an asynchronous test loop.
Next, you will create a component manifest for the test, which defines how to run the test.
Create a new component manifest for the test in
examples/diagnostics/workshop/meta/profile_store_unittests.cm
with the following contents:
{
include: [
// Needed for gtest runners
"//src/sys/test_runners/gtest/default.shard.cml",
// Needed so that logs are created
"syslog/client.shard.cml",
],
program: {
binary: "bin/profile_store_unittests",
},
use: [
{
// ProfileStore uses /data to store profiles. We'll use the tmp
// storage provided to the test.
storage: "tmp",
path: "/data",
},
],
}
Finally, add new build rules to examples/diagnostics/workshop/BUILD.gn
:
# Builds the test binary.
executable("test_bin") {
testonly = true
output_name = "profile_store_unittests"
sources = [
"profile_store_unittest.cc",
]
deps = [
":lib",
"//src/lib/fxl/test:gtest_main",
"//src/lib/testing/loop_fixture",
]
}
# Creates a test component and test package.
fuchsia_unittest_package("profile_store_unittests") {
deps = [ ":test_bin" ]
manifest = "meta/profile_store_unittests.cml"
}
# Update the existing group("tests") to include the new package as a dep
group("tests") {
testonly = true
deps = [
# new dependency
":profile_store_unittests",
":profile_store_example_unittests",
"example-integration:tests",
]
}
Next, verify that the test builds and runs.
# Build is needed the first time so that fx test becomes aware of the new test.
# For subsequent test executions, fx build is automatically invoked.
fx build examples/diagnostics/workshop:tests
fx test profile_store_unittests
You are now ready to modify the test code to verify behavior.
Adding a new integration test
The fx testgen
command autogenerates integration test boilerplate setup to use
RealmBuilder. To use it, you will need to find the compiled component manifest
of our profile_store component in our output directory.
# find the manifest in output directory.
find $(fx get-build-dir) -name profile_store.cm
# generate integration tests.
fx testgen --cm-location find result --out-dir examples/diagnostics/workshop/tests -c
This should generate a few files under examples/diagnostics/workshop/tests
. Before running the
tests, there are a few build rules that need to be updated:
- In the newly generated
examples/diagnostics/workshop/tests/BUILD.gn
- Replace
{COMPONENT_FIDL_BUILD_TARGET}
with the build target for the ProfileStore fidl -//examples/diagnostics/workshop/fidl:fuchsia.examples.diagnostics
- Replace
{COMPONENT_BUILD_TARGET}
with the build target for the ProfileStore component -//examples/diagnostics/workshop:profile_store
/
- Replace
- In
examples/diagnostics/workshop/BUILD.gn
- Add "tests" to deps in the
group("tests")
definition. This ensures GN can find the new test.
- Add "tests" to deps in the
Next, verify that the test builds and runs.
# Build is needed the first time so that fx test becomes aware of the new test.
# For subsequent test executions, fx build is automatically invoked.
fx build examples/diagnostics/workshop:tests
fx test profile_store_test
Once the test runs, you are ready to modify the boilerplate to write useful tests.