Guidelines for buildable and testable code examples

4 hours ago 1

This guide shows Upstream Pigweed maintainers how to write buildable and testable code examples for pigweed.dev. It’s focused on C++ but the general pattern is theoretically applicable to any language.

Philosophy#

Our goal is to make all code examples on pigweed.dev:

  • Buildable. The examples should be in the critical path of the “build everything” command (bazelisk build //...) so that upstream Pigweed contributors receive immediate feedback that their code changes are breaking an example that appears on pigweed.dev.

  • Testable. Each example should have one or more unit tests that verify that the example actually does what it claims to do. These tests should likewise be in the critical path of the “test everything” command (bazelisk test //...).

  • Minimal. Each example should clearly and simply demonstrate one use case. pigweed.dev readers should not need to weed through a lot of unrelated, complex code.

  • Complete. pigweed.dev readers should be able to copy-and-paste the code example and have it just work, with little or zero modification needed.

Quickstart#

Choose either Option A: Separate file for each example (recommended) or Option B: Single file for all examples. They’re mutually exclusive workflows.

Option A: Separate file for each example#

Placing each example into its own file with its own build target is the recommended approach because it provides the strongest guarantee that the example completely compiles and passes tests on its own, without interference from other examples. However, if this approach feels too toilsome, you’re welcome to use the more lightweight Option B: Single file for all examples instead.

Create an examples directory#

Create a directory named examples for your Pigweed module, e.g. //pw_string/examples/.

Having a dedicated directory makes it easy to run the unit tests for the examples with a single wildcard command, like this:

bazelisk build //pw_string/examples/...

Create the code example#

  1. Create a file for your code example. The filename should end with _test.cc, e.g. build_string_in_buffer_test.cc. The first part of the filename (build_string_in_buffer) should describe the use case.

    #define PW_LOG_MODULE_NAME "EXAMPLES_BUILD_STRING_IN_BUFFER" #include "pw_unit_test/framework.h" // DOCSTAG: [build-string-in-buffer] #include "pw_log/log.h" #include "pw_string/string_builder.h" namespace examples { void BuildStringInBuffer(pw::StringBuilder& sb) { // Add to the builder with idiomatic C++. sb << "Is it really this easy?"; sb << " YES!"; // Use the builder like any other string. PW_LOG_DEBUG("%s", sb.c_str()); } void main() { // Create a builder with a built-in buffer. std::byte buffer[64]; pw::StringBuilder sb(buffer); BuildStringInBuffer(sb); } } // namespace examples // DOCSTAG: [build-string-in-buffer] namespace { TEST(ExampleTests, BuildStringInBufferTest) { examples::main(); // Call the secondary example, just for coverage. std::byte buffer[64]; pw::StringBuilder sb(buffer); examples::BuildStringInBuffer(sb); const char* expected = "Is it really this easy? YES!"; const char* actual = sb.c_str(); EXPECT_STREQ(expected, actual); } } // namespace
Key points#

Guidelines for your code example file:

  • Only the code between the DOCSTAG comments will be pulled into the docs. See pw_string to view how the file above is actually rendered in the docs.

  • Headers that the example code relies on, like pw_log/log.h and pw_string/string_builder.h, should be shown in the user-facing code example.

  • Wrap the example code in the examples namespace.

  • The primary example code is usually wrapped in a function like BuildStringInBuffer(). This makes the example easier to unit test.

  • Each file should only contain one example. The main reason for this is to ensure that each example actually compiles on its own, without interference from other examples.

  • If you need another function to demonstrate usage of the primary example code, use void main() for the signature of this secondary function.

  • Create one or more unit tests for the primary example code. Wrap the unit test in an anonymous namespace. All code examples across all Pigweed modules should use the ExampleTests test suite.

  • Follow the usual unit testing best practice of making sure that the test initially fails.

Create build targets#

Create build targets for upstream Pigweed’s Bazel, GN, and CMake build systems.

Bazel#
  1. Create a BUILD.bazel file in your examples directory.

    load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") load("//pw_build:compatibility.bzl", "incompatible_with_mcu") load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test") licenses(["notice"]) pw_cc_test( name = "build_inlinestring_with_stringbuilder_test", srcs = ["build_inlinestring_with_stringbuilder_test.cc"], deps = [ "//pw_assert:check", "//pw_string:builder", "//pw_string:string", ], ) pw_cc_test( name = "build_string_in_buffer_test", srcs = ["build_string_in_buffer_test.cc"], deps = [ "//pw_log", "//pw_span", "//pw_string:builder", "//pw_string:string", ], ) sphinx_docs_library( name = "examples", srcs = [ "BUILD.bazel", "BUILD.gn", "CMakeLists.txt", "build_inlinestring_with_stringbuilder_test.cc", "build_string_in_buffer_test.cc", ], target_compatible_with = incompatible_with_mcu(), visibility = ["//visibility:public"], )

    The sphinx_docs_library rule is how you pull the code example into the docs build. There is no equivalent of this in the GN or CMake files because the docs are only built with Bazel.

GN#
  1. Create a BUILD.gn file in your examples directory.

    import("//build_overrides/pigweed.gni") import("$dir_pw_unit_test/test.gni") pw_test_group("tests") { tests = [ ":build_inlinestring_with_stringbuilder_test" ] } pw_test("build_inlinestring_with_stringbuilder_test") { deps = [ "$dir_pw_assert:check", "$dir_pw_string:builder", "$dir_pw_string:string", ] sources = [ "build_inlinestring_with_stringbuilder_test.cc" ] } pw_test("build_string_in_buffer_test") { deps = [ "$dir_pw_log", "$dir_pw_span", "$dir_pw_string:builder", "$dir_pw_string:string", ] sources = [ "build_string_in_buffer_test.cc" ] }
  2. Update the top-level BUILD.gn file for your module (e.g. //pw_string/BUILD.gn) so that the new code example unit tests are run as part of the module’s default unit test suite.

    pw_test_group("tests") { tests = [ ":string_test", ":format_test", ":string_builder_test", ":to_string_test", ":type_to_string_test", ":utf_codecs_test", ":util_test", "$dir_pw_string/examples:tests", ] group_deps = [ "$dir_pw_preprocessor:tests", "$dir_pw_status:tests", ] }

    Notice how $dir_pw_string/examples:tests is included in the list of tests.

CMake#
  1. Create a CMakeLists.txt file in your examples directory.

    include("$ENV{PW_ROOT}/pw_build/pigweed.cmake") pw_add_test(pw_string.examples.build_inlinestring_with_stringbuilder_test PRIVATE_DEPS pw_assert.check pw_string.builder pw_string.string SOURCES build_inlinestring_with_stringbuilder_test.cc GROUPS modules pw_string ) pw_add_test(pw_string.examples.build_string_in_buffer_test PRIVATE_DEPS pw_log pw_span pw_string.builder pw_string.string SOURCES build_inlinestring_with_stringbuilder_test.cc GROUPS modules pw_string )

Pull the example into a doc#

  1. In your module’s top-level BUILD.bazel file (e.g. //pw_string/BUILD.bazel), update the sphinx_docs_library target:

    sphinx_docs_library( name = "docs", srcs = [ "BUILD.bazel", "BUILD.gn", "Kconfig", "api.rst", "code_size.rst", "design.rst", "docs.rst", "guide.rst", ":format_size_report", ":string_builder_size_report", "//pw_string/examples", ], prefix = "pw_string/", target_compatible_with = incompatible_with_mcu(), )

    Notice how //pw_string/examples is included in the deps of the docs target. This is how you make the example source code available to Sphinx when it builds the docs.

  2. Use a literalinclude directive in your reStructuredText to pull the code example into your doc:

    .. literalinclude:: ./examples/build_string_in_buffer_test.cc :language: cpp :dedent: :start-after: // DOCSTAG: [build-string-in-buffer] :end-before: // DOCSTAG: [build-string-in-buffer]

You’re done!

Option B: Single file for all examples#

In the Option B approach you place all of your examples and unit tests in a single file and build target. The main drawback with this approach is that it’s easy to accidentally make your code example incomplete. E.g. you forget to include a header in an example, because an earlier example already included that same header. The build target is also harder to read, because all of the dependencies for all of the code examples are mixed into a single target.

However, Option B is still a major improvement over the status quo of not building or testing code examples, so you’re welcome to use Option B if Option A: Separate file for each example feels too toilsome.

Create a file for the code examples#

  1. Create an examples.cc file in the root directory of your module. All of your code examples and unit tests will go in this single file.

    #include "pw_unit_test/framework.h" // DOCSTAG[pw_assert-mod-example] #include <functional> #include "pw_assert/check.h" namespace examples { void CheckValueIsOdd(int value) { // This will not compile due to use of the % character: // PW_CHECK(value % 2 != 0); // Instead, store the result in a variable. const int mod_2 = value % 2; PW_CHECK_INT_NE(mod_2, 0); // Or, perform the % operation in a function, such as std::modulus. PW_CHECK_INT_NE(std::modulus{}(value, 2), 0); } } // namespace examples // DOCSTAG[pw_assert-mod-example] namespace { TEST(AssertExamples, CheckOrAssertValueIsOdd) { examples::CheckValueIsOdd(1); examples::CheckValueIsOdd(3); } } // namespace
  2. Make sure that your code examples and unit tests follow all of the guidelines that are described in Key points.

Create build targets#

  1. In your module’s top-level build files (e.g. //pw_assert/BUILD.bazel, //pw_assert/BUILD.gn, and //pw_assert/CMakeLists.txt) create build targets for your new examples.cc file.

    Here are examples for each build system:

    Bazel:

    pw_cc_test( name = "examples", srcs = ["examples.cc"], deps = [":check"], ) sphinx_docs_library( name = "docs", srcs = [ "BUILD.bazel", "BUILD.gn", "CMakeLists.txt", "assert_test.cc", "backends.rst", "docs.rst", "examples.cc", ], prefix = "pw_assert/", target_compatible_with = incompatible_with_mcu(), )

    GN:

    pw_test_group("tests") { tests = [ ":assert_test", ":assert_backend_compile_test", ":assert_facade_test", ":examples", ] } pw_test("examples") { sources = [ "examples.cc" ] deps = [ ":check" ] }

    CMake:

    pw_add_test(pw_assert.examples SOURCES examples.cc PRIVATE_DEPS pw_assert.check GROUPS modules pw_assert )

Pull the example into a doc#

  1. Use a literalinclude directive in your reStructuredText to pull the code example into your doc:

    .. literalinclude:: examples.cc :language: cpp :start-after: [pw_assert-mod-example] :end-before: [pw_assert-mod-example]

You’re done!

Read Entire Article